mirror of
https://github.com/1f349/violet.git
synced 2025-04-15 15:17:58 +01:00
Compare commits
87 Commits
Author | SHA1 | Date | |
---|---|---|---|
d3d6782b22 | |||
0f095056d4 | |||
aa77dccaaf | |||
ecee594219 | |||
d7b7721378 | |||
3e86b91ec3 | |||
f442409ebf | |||
8aa82303ce | |||
1f4f4414d5 | |||
a8db73d957 | |||
1181fde508 | |||
900203b560 | |||
69bce2d12d | |||
a13db89c44 | |||
e901a73129 | |||
333394cf89 | |||
f8dde8eebe | |||
822c7b570a | |||
bc6e98db8c | |||
2cce26429b | |||
5643f05aa0 | |||
fc2f3d5b7b | |||
37b0617e78 | |||
1194717a32 | |||
11b989b50c | |||
30bcea40b8 | |||
69670e068b | |||
c91f1dd2fc | |||
754fd2d396 | |||
3834787f8f | |||
52547234b0 | |||
6559b21c16 | |||
f268656b20 | |||
e87809849e | |||
ef5a15f5c3 | |||
78d930d32c | |||
3141b3bc55 | |||
6a5be76db3 | |||
221a46cc55 | |||
f1a2ce0896 | |||
0dab17ea85 | |||
a16617b131 | |||
d19050060a | |||
8c4aa67e7b | |||
869d114891 | |||
53041f4ac7 | |||
b5ff809345 | |||
fbbcf0440c | |||
1f72795f22 | |||
eddef80671 | |||
f3c641b82d | |||
cf098eb0b9 | |||
ce12384c15 | |||
b84df84d51 | |||
1ca5a6205b | |||
dfdc2b2c11 | |||
8ab677964d | |||
dcef716d8f | |||
d659d71ba4 | |||
b2a53722e6 | |||
eb8e4c7ed7 | |||
ce8c421187 | |||
949dcd298a | |||
c930ddff28 | |||
755e597a11 | |||
43eb689254 | |||
17cd9a9d64 | |||
eb8954f794 | |||
ab36a39917 | |||
76e37f7af9 | |||
7e05271a79 | |||
ba1c0d4129 | |||
a63dd95201 | |||
92fa325fd5 | |||
629057edc3 | |||
e9db9d6ef2 | |||
d0149c87dc | |||
25c9a87068 | |||
b70c63dbbb | |||
c64d89eb5c | |||
a432ba8027 | |||
f6d3abb515 | |||
80a03709d3 | |||
ee2f1e9d5a | |||
afc661c62b | |||
1f487eb80c | |||
9899d67d50 |
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@ -4,13 +4,15 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.20.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
go-version: [1.22.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- uses: actions/checkout@v3
|
||||
- run: sudo add-apt-repository ppa:inkscape.dev/stable
|
||||
- run: sudo apt-get update
|
||||
- run: sudo apt-get install inkscape -y
|
||||
- run: go build ./cmd/violet/
|
||||
- run: go test ./...
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,4 @@
|
||||
*.sqlite
|
||||
*.local
|
||||
.idea/
|
||||
.data
|
||||
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
5
.idea/codeStyles/codeStyleConfig.xml
generated
5
.idea/codeStyles/codeStyleConfig.xml
generated
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
12
.idea/dataSources.xml
generated
12
.idea/dataSources.xml
generated
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="__db.sqlite" uuid="5aeb4e88-8ec4-4227-a921-ba4eaed357bf">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:__db.sqlite</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
7
.idea/discord.xml
generated
7
.idea/discord.xml
generated
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
6
.idea/misc.xml
generated
6
.idea/misc.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettingsMigration">
|
||||
<option name="stateVersion" value="1" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/violet.iml" filepath="$PROJECT_DIR$/.idea/violet.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/sqldialects.xml
generated
6
.idea/sqldialects.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
9
.idea/violet.iml
generated
9
.idea/violet.iml
generated
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
674
LICENSE.txt
Normal file
674
LICENSE.txt
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
@ -1,12 +0,0 @@
|
||||
module benchmarks
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/MrMelon54/violet v0.0.0-20230419182034-77d570ac1e6d
|
||||
github.com/gorilla/mux v1.8.0
|
||||
)
|
||||
|
||||
require github.com/MrMelon54/trie v0.0.2 // indirect
|
||||
|
||||
replace github.com/MrMelon54/violet => ../
|
@ -1,8 +0,0 @@
|
||||
github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY=
|
||||
github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
@ -1,42 +0,0 @@
|
||||
package benchmarks
|
||||
|
||||
import (
|
||||
"github.com/MrMelon54/violet/router"
|
||||
"github.com/MrMelon54/violet/target"
|
||||
gorillaRouter "github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func benchRequest(b *testing.B, router http.Handler, r *http.Request) {
|
||||
w := httptest.NewRecorder()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
if w.Header().Get("Location") != "https://example.com" {
|
||||
b.Fatal("Location: ", w.Header().Get("Location"), " != https://example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVioletRouter(b *testing.B) {
|
||||
r := router.New(nil)
|
||||
r.AddRedirect("*.example.com", "", target.Redirect{
|
||||
Pre: true,
|
||||
Host: "example.com",
|
||||
Code: http.StatusPermanentRedirect,
|
||||
})
|
||||
benchRequest(b, r, httptest.NewRequest(http.MethodGet, "https://www.example.com", nil))
|
||||
}
|
||||
|
||||
func BenchmarkGorillaMux(b *testing.B) {
|
||||
r := gorillaRouter.NewRouter()
|
||||
r.Host("{subdomain}.example.com").Handler(target.Redirect{
|
||||
Pre: true,
|
||||
Host: "example.com",
|
||||
Code: http.StatusPermanentRedirect,
|
||||
})
|
||||
benchRequest(b, r, httptest.NewRequest(http.MethodGet, "https://www.example.com/", nil))
|
||||
}
|
113
certs/certs.go
113
certs/certs.go
@ -1,20 +1,24 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/certgen"
|
||||
"crypto/tls"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/mrmelon54/certgen"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"io/fs"
|
||||
"log"
|
||||
"math/big"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Certs")
|
||||
|
||||
// Certs is the certificate loader and management system.
|
||||
type Certs struct {
|
||||
cDir fs.FS
|
||||
@ -24,6 +28,9 @@ type Certs struct {
|
||||
m map[string]*tls.Certificate
|
||||
ca *certgen.CertGen
|
||||
sn atomic.Int64
|
||||
r *rescheduler.Rescheduler
|
||||
t *time.Ticker
|
||||
ts chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new cert list
|
||||
@ -34,23 +41,40 @@ func New(certDir fs.FS, keyDir fs.FS, selfCert bool) *Certs {
|
||||
ss: selfCert,
|
||||
s: &sync.RWMutex{},
|
||||
m: make(map[string]*tls.Certificate),
|
||||
ts: make(chan struct{}, 1),
|
||||
}
|
||||
if c.ss {
|
||||
ca, err := certgen.MakeCaTls(pkix.Name{
|
||||
|
||||
if !selfCert {
|
||||
// the rescheduler isn't even used in self cert mode so why initialise it
|
||||
c.r = rescheduler.NewRescheduler(c.threadCompile)
|
||||
|
||||
c.t = time.NewTicker(2 * time.Hour)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.t.C:
|
||||
c.Compile()
|
||||
case <-c.ts:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
// in self-signed mode generate a CA certificate to sign other certificates
|
||||
ca, err := certgen.MakeCaTls(4096, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Organization: []string{"Violet"},
|
||||
OrganizationalUnit: []string{"Development"},
|
||||
SerialNumber: "0",
|
||||
CommonName: fmt.Sprintf("%d.violet.test", time.Now().Unix()),
|
||||
}, big.NewInt(0))
|
||||
}, big.NewInt(0), func(now time.Time) time.Time {
|
||||
return now.AddDate(10, 0, 0)
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate CA cert for self-signed mode:", err)
|
||||
logger.Logger.Fatal("Failed to generate CA cert for self-signed mode", "err", err)
|
||||
}
|
||||
c.ca = ca
|
||||
}
|
||||
|
||||
// run compile to get the initial data
|
||||
c.Compile()
|
||||
return c
|
||||
}
|
||||
|
||||
@ -67,16 +91,20 @@ func (c *Certs) GetCertForDomain(domain string) *tls.Certificate {
|
||||
// if self-signed certificate is enabled then generate a certificate
|
||||
if c.ss {
|
||||
sn := c.sn.Add(1)
|
||||
serverTls, err := certgen.MakeServerTls(c.ca, pkix.Name{
|
||||
serverTls, err := certgen.MakeServerTls(c.ca, 4096, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Organization: []string{domain},
|
||||
OrganizationalUnit: []string{domain},
|
||||
SerialNumber: fmt.Sprintf("%d", sn),
|
||||
CommonName: domain,
|
||||
}, big.NewInt(sn), []string{domain}, nil)
|
||||
}, big.NewInt(sn), func(now time.Time) time.Time {
|
||||
return now.AddDate(10, 0, 0)
|
||||
}, []string{domain}, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// save the generated leaf for loading if the domain is requested again
|
||||
leaf := serverTls.GetTlsLeaf()
|
||||
c.m[domain] = &leaf
|
||||
return &leaf
|
||||
@ -93,41 +121,56 @@ func (c *Certs) GetCertForDomain(domain string) *tls.Certificate {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile loads the certificates and keys from the directories.
|
||||
//
|
||||
// This method makes use of the rescheduler instead of just ignoring multiple
|
||||
// calls.
|
||||
func (c *Certs) Compile() {
|
||||
// don't bother compiling in self-signed mode
|
||||
if c.ss {
|
||||
return
|
||||
}
|
||||
c.r.Run()
|
||||
}
|
||||
|
||||
// async compile magic
|
||||
go func() {
|
||||
// new map
|
||||
certMap := make(map[string]*tls.Certificate)
|
||||
func (c *Certs) Stop() {
|
||||
if c.t != nil {
|
||||
c.t.Stop()
|
||||
}
|
||||
close(c.ts)
|
||||
}
|
||||
|
||||
// compile map and check errors
|
||||
err := c.internalCompile(certMap)
|
||||
if err != nil {
|
||||
log.Printf("[Certs] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
func (c *Certs) threadCompile() {
|
||||
// new map
|
||||
certMap := make(map[string]*tls.Certificate)
|
||||
|
||||
// lock while replacing the map
|
||||
c.s.Lock()
|
||||
c.m = certMap
|
||||
c.s.Unlock()
|
||||
}()
|
||||
// compile map and check errors
|
||||
err := c.internalCompile(certMap)
|
||||
if err != nil {
|
||||
Logger.Infof("Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// lock while replacing the map
|
||||
c.s.Lock()
|
||||
c.m = certMap
|
||||
c.s.Unlock()
|
||||
}
|
||||
|
||||
// internalCompile is a hidden internal method for loading the certificate and
|
||||
// key files
|
||||
func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
|
||||
if c.cDir == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// try to read dir
|
||||
files, err := fs.ReadDir(c.cDir, "")
|
||||
files, err := fs.ReadDir(c.cDir, ".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read cert dir: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[Certs] Compiling lookup table for %d certificates\n", len(files))
|
||||
Logger.Infof("Compiling lookup table for %d certificates\n", len(files))
|
||||
|
||||
// find and parse certs
|
||||
for _, i := range files {
|
||||
@ -138,8 +181,10 @@ func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
|
||||
|
||||
// get file name and extension
|
||||
name := i.Name()
|
||||
ext := filepath.Ext(name)
|
||||
keyName := name[:len(name)-len(ext)] + "key"
|
||||
if !strings.HasSuffix(name, ".cert.pem") {
|
||||
continue
|
||||
}
|
||||
keyName := name[:len(name)-len("cert.pem")] + "key.pem"
|
||||
|
||||
// try to read cert file
|
||||
certData, err := fs.ReadFile(c.cDir, name)
|
||||
@ -150,6 +195,10 @@ func (c *Certs) internalCompile(m map[string]*tls.Certificate) error {
|
||||
// try to read key file
|
||||
keyData, err := fs.ReadFile(c.kDir, keyName)
|
||||
if err != nil {
|
||||
// ignore the file if the certificate doesn't exist
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to read key file '%s': %w", keyName, err)
|
||||
}
|
||||
|
||||
|
78
certs/certs_test.go
Normal file
78
certs/certs_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
package certs
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"github.com/mrmelon54/certgen"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/big"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCertsNew_Lookup(t *testing.T) {
|
||||
// The following code basically copies the self-signed logic from the Certs
|
||||
// type to test that certificate files can be found and read correctly. This
|
||||
// uses a MapFS for performance during tests.
|
||||
|
||||
ca, err := certgen.MakeCaTls(2048, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Organization: []string{"Violet"},
|
||||
OrganizationalUnit: []string{"Development"},
|
||||
SerialNumber: "0",
|
||||
CommonName: fmt.Sprintf("%d.violet.test", time.Now().Unix()),
|
||||
}, big.NewInt(0), func(now time.Time) time.Time {
|
||||
return now.AddDate(10, 0, 0)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
domain := "example.com"
|
||||
sn := int64(1)
|
||||
serverTls, err := certgen.MakeServerTls(ca, 2048, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Organization: []string{domain},
|
||||
OrganizationalUnit: []string{domain},
|
||||
SerialNumber: fmt.Sprintf("%d", sn),
|
||||
CommonName: domain,
|
||||
}, big.NewInt(sn), func(now time.Time) time.Time {
|
||||
return now.AddDate(10, 0, 0)
|
||||
}, []string{domain}, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
certDir := fstest.MapFS{
|
||||
"example.com.cert.pem": {
|
||||
Data: serverTls.GetCertPem(),
|
||||
},
|
||||
}
|
||||
|
||||
keyDir := fstest.MapFS{
|
||||
"example.com.key.pem": {
|
||||
Data: serverTls.GetKeyPem(),
|
||||
},
|
||||
}
|
||||
|
||||
certs := New(certDir, keyDir, false)
|
||||
assert.NoError(t, certs.internalCompile(certs.m))
|
||||
cc := certs.GetCertForDomain("example.com")
|
||||
leaf := certgen.TlsLeaf(cc)
|
||||
assert.Equal(t, []string{"example.com"}, leaf.DNSNames)
|
||||
|
||||
// this cert doesn't exist
|
||||
assert.Nil(t, certs.GetCertForDomain("notexample.com"))
|
||||
}
|
||||
|
||||
func TestCertsNew_SelfSigned(t *testing.T) {
|
||||
if testing.Short() {
|
||||
return
|
||||
}
|
||||
|
||||
certs := New(nil, nil, true)
|
||||
cc := certs.GetCertForDomain("example.com")
|
||||
leaf := certgen.TlsLeaf(cc)
|
||||
assert.Equal(t, []string{"example.com"}, leaf.DNSNames)
|
||||
|
||||
cc2 := certs.GetCertForDomain("notexample.com")
|
||||
leaf2 := certgen.TlsLeaf(cc2)
|
||||
assert.Equal(t, []string{"notexample.com"}, leaf2.DNSNames)
|
||||
}
|
15
cmd/violet/conf.go
Normal file
15
cmd/violet/conf.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
type startUpConfig struct {
|
||||
SelfSigned bool `json:"self_signed"`
|
||||
ErrorPagePath string `json:"error_page_path"`
|
||||
Listen listenConfig `json:"listen"`
|
||||
InkscapeCmd string `json:"inkscape"`
|
||||
RateLimit uint64 `json:"rate_limit"`
|
||||
}
|
||||
|
||||
type listenConfig struct {
|
||||
Api string `json:"api"`
|
||||
Http string `json:"http"`
|
||||
Https string `json:"https"`
|
||||
}
|
@ -1,118 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/certs"
|
||||
"github.com/MrMelon54/violet/domains"
|
||||
errorPages "github.com/MrMelon54/violet/error-pages"
|
||||
"github.com/MrMelon54/violet/favicons"
|
||||
"github.com/MrMelon54/violet/proxy"
|
||||
"github.com/MrMelon54/violet/router"
|
||||
"github.com/MrMelon54/violet/servers"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/google/subcommands"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// flags - each one has a usage field lol
|
||||
var (
|
||||
databasePath = flag.String("db", "", "/path/to/database.sqlite : path to the database file")
|
||||
keyPath = flag.String("keys", "", "/path/to/keys : path contains the keys with names matching the certificates and '.key' extensions")
|
||||
certPath = flag.String("certs", "", "/path/to/certificates : path contains the certificates to load in armoured PEM encoding")
|
||||
selfSigned = flag.Bool("ss", false, "enable self-signed certificate mode")
|
||||
errorPagePath = flag.String("errors", "", "/path/to/error-pages : path contains the custom error pages")
|
||||
apiListen = flag.String("api", "127.0.0.1:8080", "address for api listening")
|
||||
httpListen = flag.String("http", "0.0.0.0:80", "address for http listening")
|
||||
httpsListen = flag.String("https", "0.0.0.0:443", "address for https listening")
|
||||
inkscapeCmd = flag.String("inkscape", "inkscape", "Path to inkscape binary")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("[Violet] Starting...")
|
||||
subcommands.Register(subcommands.HelpCommand(), "")
|
||||
subcommands.Register(subcommands.FlagsCommand(), "")
|
||||
subcommands.Register(subcommands.CommandsCommand(), "")
|
||||
subcommands.Register(&serveCmd{}, "")
|
||||
subcommands.Register(&setupCmd{}, "")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *certPath != "" {
|
||||
// create path to cert dir
|
||||
err := os.MkdirAll(*certPath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to create certificate path '%s' does not exist", *certPath)
|
||||
}
|
||||
}
|
||||
if *keyPath != "" {
|
||||
// create path to key dir
|
||||
err := os.MkdirAll(*keyPath, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to create certificate key path '%s' does not exist", *keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := sql.Open("sqlite3", *databasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("[Violet] Failed to open database '%s'...", *databasePath)
|
||||
}
|
||||
|
||||
allowedDomains := domains.New(db) // load allowed domains
|
||||
allowedCerts := certs.New(os.DirFS(*certPath), os.DirFS(*keyPath), *selfSigned) // load certificate manager
|
||||
reverseProxy := proxy.CreateHybridReverseProxy() // load reverse proxy
|
||||
dynamicFavicons := favicons.New(db, *inkscapeCmd) // load dynamic favicon provider
|
||||
dynamicErrorPages := errorPages.New(os.DirFS(*errorPagePath)) // load dynamic error page provider
|
||||
dynamicRouter := router.NewManager(db, reverseProxy) // load dynamic router manager
|
||||
|
||||
// struct containing config for the http servers
|
||||
srvConf := &servers.Conf{
|
||||
ApiListen: *apiListen,
|
||||
HttpListen: *httpListen,
|
||||
HttpsListen: *httpsListen,
|
||||
DB: db,
|
||||
Domains: allowedDomains,
|
||||
Certs: allowedCerts,
|
||||
Favicons: dynamicFavicons,
|
||||
Verify: nil, // TODO: add mjwt verify support
|
||||
ErrorPages: dynamicErrorPages,
|
||||
Router: dynamicRouter,
|
||||
}
|
||||
|
||||
var srvApi, srvHttp, srvHttps *http.Server
|
||||
if *apiListen != "" {
|
||||
srvApi = servers.NewApiServer(srvConf, utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter})
|
||||
}
|
||||
if *httpListen != "" {
|
||||
srvHttp = servers.NewHttpServer(srvConf)
|
||||
}
|
||||
if *httpsListen != "" {
|
||||
srvHttps = servers.NewHttpsServer(srvConf)
|
||||
}
|
||||
|
||||
// Wait for exit signal
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
||||
<-sc
|
||||
fmt.Println()
|
||||
|
||||
// Stop servers
|
||||
log.Printf("[Violet] Stopping...")
|
||||
n := time.Now()
|
||||
|
||||
// close http servers
|
||||
if srvApi != nil {
|
||||
srvApi.Close()
|
||||
}
|
||||
if srvHttp != nil {
|
||||
srvHttp.Close()
|
||||
}
|
||||
if srvHttps != nil {
|
||||
srvHttps.Close()
|
||||
}
|
||||
|
||||
log.Printf("[Violet] Took '%s' to shutdown\n", time.Now().Sub(n))
|
||||
log.Println("[Violet] Goodbye")
|
||||
ctx := context.Background()
|
||||
os.Exit(int(subcommands.Execute(ctx)))
|
||||
}
|
||||
|
249
cmd/violet/serve.go
Normal file
249
cmd/violet/serve.go
Normal file
@ -0,0 +1,249 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/certs"
|
||||
"github.com/1f349/violet/domains"
|
||||
errorPages "github.com/1f349/violet/error-pages"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/servers"
|
||||
"github.com/1f349/violet/servers/api"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/cloudflare/tableflip"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type serveCmd struct {
|
||||
configPath string
|
||||
debugLog bool
|
||||
pidFile string
|
||||
}
|
||||
|
||||
func (s *serveCmd) Name() string { return "serve" }
|
||||
func (s *serveCmd) Synopsis() string { return "Serve reverse proxy server" }
|
||||
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
|
||||
f.BoolVar(&s.debugLog, "debug", false, "enable debug logging")
|
||||
f.StringVar(&s.pidFile, "pid-file", "", "path to pid file")
|
||||
}
|
||||
func (s *serveCmd) Usage() string {
|
||||
return `serve [-conf <config file>] [-debug] [-pid-file <pid file>]
|
||||
Serve reverse proxy server using information from config file
|
||||
`
|
||||
}
|
||||
|
||||
func (s *serveCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
if s.debugLog {
|
||||
logger.Logger.SetLevel(log.DebugLevel)
|
||||
}
|
||||
logger.Logger.Info("Starting...")
|
||||
|
||||
upg, err := tableflip.New(tableflip.Options{
|
||||
PIDFile: s.pidFile,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer upg.Stop()
|
||||
|
||||
if s.configPath == "" {
|
||||
logger.Logger.Info("Error: config flag is missing")
|
||||
return subcommands.ExitUsageError
|
||||
}
|
||||
|
||||
openConf, err := os.Open(s.configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logger.Logger.Info("Error: missing config file")
|
||||
} else {
|
||||
logger.Logger.Info("Error: open config file: ", err)
|
||||
}
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
var config startUpConfig
|
||||
err = json.NewDecoder(openConf).Decode(&config)
|
||||
if err != nil {
|
||||
logger.Logger.Info("Error: invalid config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// working directory is the parent of the config file
|
||||
wd := filepath.Dir(s.configPath)
|
||||
|
||||
// the cert and key paths are useless in self-signed mode
|
||||
if !config.SelfSigned {
|
||||
// create path to cert dir
|
||||
err := os.MkdirAll(filepath.Join(wd, "certs"), os.ModePerm)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to create certificate path")
|
||||
}
|
||||
// create path to key dir
|
||||
err = os.MkdirAll(filepath.Join(wd, "keys"), os.ModePerm)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to create certificate key path")
|
||||
}
|
||||
}
|
||||
|
||||
// errorPageDir stores an FS interface for accessing the error page directory
|
||||
var errorPageDir fs.FS
|
||||
if config.ErrorPagePath != "" {
|
||||
errorPageDir = os.DirFS(config.ErrorPagePath)
|
||||
err := os.MkdirAll(config.ErrorPagePath, os.ModePerm)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to create error page", "path", config.ErrorPagePath)
|
||||
}
|
||||
}
|
||||
|
||||
// load the MJWT RSA public key from a pem encoded file
|
||||
mJwtVerify, err := mjwt.NewMJwtVerifierFromFile(filepath.Join(wd, "signer.public.pem"))
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to load MJWT verifier public key", "file", filepath.Join(wd, "signer.public.pem"), "err", err)
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := violet.InitDB(filepath.Join(wd, "violet.db.sqlite"))
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to open database", "err", err)
|
||||
}
|
||||
|
||||
certDir := os.DirFS(filepath.Join(wd, "certs"))
|
||||
keyDir := os.DirFS(filepath.Join(wd, "keys"))
|
||||
|
||||
// setup registry for metrics
|
||||
promRegistry := prometheus.NewRegistry()
|
||||
promRegistry.MustRegister(
|
||||
collectors.NewGoCollector(),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||
)
|
||||
|
||||
ws := websocket.NewServer()
|
||||
allowedDomains := domains.New(db) // load allowed domains
|
||||
acmeChallenges := utils.NewAcmeChallenge() // load acme challenge store
|
||||
allowedCerts := certs.New(certDir, keyDir, config.SelfSigned) // load certificate manager
|
||||
hybridTransport := proxy.NewHybridTransport(ws) // load reverse proxy
|
||||
dynamicFavicons := favicons.New(db, config.InkscapeCmd) // load dynamic favicon provider
|
||||
dynamicErrorPages := errorPages.New(errorPageDir) // load dynamic error page provider
|
||||
dynamicRouter := router.NewManager(db, hybridTransport) // load dynamic router manager
|
||||
|
||||
// struct containing config for the http servers
|
||||
srvConf := &conf.Conf{
|
||||
RateLimit: config.RateLimit,
|
||||
DB: db,
|
||||
Domains: allowedDomains,
|
||||
Acme: acmeChallenges,
|
||||
Certs: allowedCerts,
|
||||
Favicons: dynamicFavicons,
|
||||
Signer: mJwtVerify,
|
||||
ErrorPages: dynamicErrorPages,
|
||||
Router: dynamicRouter,
|
||||
}
|
||||
|
||||
// create the compilable list and run a first time compile
|
||||
allCompilables := utils.MultiCompilable{allowedDomains, allowedCerts, dynamicFavicons, dynamicErrorPages, dynamicRouter}
|
||||
allCompilables.Compile()
|
||||
|
||||
_, httpsPort, ok := utils.SplitDomainPort(config.Listen.Https, 443)
|
||||
if !ok {
|
||||
httpsPort = 443
|
||||
}
|
||||
|
||||
var srvApi, srvHttp, srvHttps *http.Server
|
||||
if config.Listen.Api != "" {
|
||||
// Listen must be called before Ready
|
||||
lnApi, err := upg.Listen("tcp", config.Listen.Api)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Listen failed", "err", err)
|
||||
}
|
||||
srvApi = api.NewApiServer(srvConf, allCompilables, promRegistry)
|
||||
srvApi.SetKeepAlivesEnabled(false)
|
||||
l := logger.Logger.With("server", "API")
|
||||
l.Info("Starting server", "addr", config.Listen.Api)
|
||||
go utils.RunBackgroundHttp(l, srvApi, lnApi)
|
||||
}
|
||||
if config.Listen.Http != "" {
|
||||
// Listen must be called before Ready
|
||||
lnHttp, err := upg.Listen("tcp", config.Listen.Http)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Listen failed", "err", err)
|
||||
}
|
||||
srvHttp = servers.NewHttpServer(uint16(httpsPort), srvConf, promRegistry)
|
||||
srvHttp.SetKeepAlivesEnabled(false)
|
||||
l := logger.Logger.With("server", "HTTP")
|
||||
l.Info("Starting server", "addr", config.Listen.Http)
|
||||
go utils.RunBackgroundHttp(l, srvHttp, lnHttp)
|
||||
}
|
||||
if config.Listen.Https != "" {
|
||||
// Listen must be called before Ready
|
||||
lnHttps, err := upg.Listen("tcp", config.Listen.Https)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Listen failed", "err", err)
|
||||
}
|
||||
srvHttps = servers.NewHttpsServer(srvConf, promRegistry)
|
||||
srvHttps.SetKeepAlivesEnabled(false)
|
||||
l := logger.Logger.With("server", "HTTPS")
|
||||
l.Info("Starting server", "addr", config.Listen.Https)
|
||||
go utils.RunBackgroundHttps(l, srvHttps, lnHttps)
|
||||
}
|
||||
|
||||
// Do an upgrade on SIGHUP
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGHUP)
|
||||
for range sig {
|
||||
err := upg.Upgrade()
|
||||
if err != nil {
|
||||
logger.Logger.Error("Failed upgrade", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Logger.Info("Ready")
|
||||
if err := upg.Ready(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
<-upg.Exit()
|
||||
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
logger.Logger.Warn("Graceful shutdown timed out")
|
||||
os.Exit(1)
|
||||
})
|
||||
|
||||
// stop updating certificates
|
||||
allowedCerts.Stop()
|
||||
|
||||
// close websockets first
|
||||
ws.Shutdown()
|
||||
|
||||
// close http servers
|
||||
if srvApi != nil {
|
||||
_ = srvApi.Close()
|
||||
}
|
||||
if srvHttp != nil {
|
||||
_ = srvHttp.Close()
|
||||
}
|
||||
if srvHttps != nil {
|
||||
_ = srvHttps.Close()
|
||||
}
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
223
cmd/violet/setup.go
Normal file
223
cmd/violet/setup.go
Normal file
@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/domains"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/google/subcommands"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type setupCmd struct {
|
||||
wdPath string
|
||||
}
|
||||
|
||||
func (s *setupCmd) Name() string { return "setup" }
|
||||
func (s *setupCmd) Synopsis() string { return "Walkthrough creating a config file" }
|
||||
func (s *setupCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&s.wdPath, "wd", ".", "Path to the directory to create config files in (defaults to the working directory)")
|
||||
}
|
||||
func (s *setupCmd) Usage() string {
|
||||
return `setup
|
||||
Setup Violet automatically by answering questions.
|
||||
`
|
||||
}
|
||||
|
||||
func (s *setupCmd) Execute(_ context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
// get absolute path to specify files
|
||||
wdAbs, err := filepath.Abs(s.wdPath)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to get full directory path: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// ask about running the setup steps
|
||||
createFile := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Violet config files in this directory: '%s'?", wdAbs)}, &createFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
if !createFile {
|
||||
fmt.Println("Goodbye")
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
// store answers from questions
|
||||
var answers struct {
|
||||
SelfSigned bool
|
||||
ErrorPages bool
|
||||
ApiListen string
|
||||
HttpListen string
|
||||
HttpsListen string
|
||||
RateLimit uint64
|
||||
FirstDomain string
|
||||
ApiUrl string
|
||||
}
|
||||
|
||||
// ask main questions
|
||||
err = survey.Ask([]*survey.Question{
|
||||
{
|
||||
Name: "SelfSigned",
|
||||
Prompt: &survey.Confirm{Message: "Enable self-signed certificate?"},
|
||||
},
|
||||
{
|
||||
Name: "ErrorPages",
|
||||
Prompt: &survey.Confirm{Message: "Enable custom error pages?"},
|
||||
},
|
||||
{
|
||||
Name: "ApiListen",
|
||||
Prompt: &survey.Input{Message: "API listen address", Default: "127.0.0.1:8080"},
|
||||
Validate: listenAddressValidator,
|
||||
},
|
||||
{
|
||||
Name: "HttpListen",
|
||||
Prompt: &survey.Input{Message: "HTTP listen address", Default: ":80"},
|
||||
},
|
||||
{
|
||||
Name: "HttpsListen",
|
||||
Prompt: &survey.Input{Message: "HTTPS listen address", Default: ":443"},
|
||||
},
|
||||
{
|
||||
Name: "RateLimit",
|
||||
Prompt: &survey.Input{Message: "Rate limit", Default: "300", Help: "Number of allowed requests per minute per IP"},
|
||||
Validate: func(ans interface{}) error {
|
||||
if ansStr, ok := ans.(string); ok {
|
||||
_, err := strconv.ParseUint(ansStr, 10, 64)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "FirstDomain",
|
||||
Prompt: &survey.Input{Message: "First domain", Default: "example.com", Help: "Setup the first domain or it will be more difficult to setup later"},
|
||||
Validate: survey.Required,
|
||||
},
|
||||
}, &answers)
|
||||
if err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// generate database path
|
||||
databaseFile := filepath.Join(wdAbs, "violet.db.sqlite")
|
||||
errorPagePath := ""
|
||||
if answers.ErrorPages {
|
||||
errorPagePath = filepath.Join(wdAbs, "error-pages")
|
||||
}
|
||||
|
||||
// write config file
|
||||
confFile := filepath.Join(wdAbs, "config.json")
|
||||
createConf, err := os.Create(confFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
confEncode := json.NewEncoder(createConf)
|
||||
confEncode.SetIndent("", " ")
|
||||
err = confEncode.Encode(startUpConfig{
|
||||
SelfSigned: answers.SelfSigned,
|
||||
ErrorPagePath: errorPagePath,
|
||||
Listen: listenConfig{
|
||||
Api: answers.ApiListen,
|
||||
Http: answers.HttpListen,
|
||||
Https: answers.HttpsListen,
|
||||
},
|
||||
InkscapeCmd: "inkscape",
|
||||
RateLimit: answers.RateLimit,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to write config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := violet.InitDB(databaseFile)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to open database", "err", err)
|
||||
}
|
||||
|
||||
// domain manager to add a domain, no need to compile here as the program needs
|
||||
// to be run again with the serve subcommand
|
||||
allowedDomains := domains.New(db)
|
||||
allowedDomains.Put(answers.FirstDomain, true)
|
||||
|
||||
// don't bother with this part is the api won't be listening
|
||||
if answers.ApiListen != "" {
|
||||
// ask for url
|
||||
err = survey.AskOne(&survey.Input{Message: "API URL", Default: "api.example.com/violet", Help: "Enter the URL which should point to the internal Violet API"}, &answers.ApiUrl, survey.WithValidator(func(ans interface{}) error {
|
||||
if ansStr, ok := ans.(string); ok {
|
||||
_, err := url.Parse(ansStr)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Println("Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// parse the api url
|
||||
apiUrl, err := url.Parse(answers.ApiUrl)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to parse API URL: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
// add with the route manager, no need to compile as this will run when opened
|
||||
// with the serve subcommand
|
||||
routeManager := router.NewManager(db, proxy.NewHybridTransportWithCalls(&nilTransport{}, &nilTransport{}, &websocket.Server{}))
|
||||
err = routeManager.InsertRoute(target.RouteWithActive{
|
||||
Route: target.Route{
|
||||
Src: path.Join(apiUrl.Host, apiUrl.Path),
|
||||
Dst: answers.ApiListen,
|
||||
Flags: target.FlagPre | target.FlagCors | target.FlagForwardHost | target.FlagForwardAddr,
|
||||
},
|
||||
Active: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to insert api route into database: ", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Setup complete")
|
||||
fmt.Printf("Run the reverse proxy with `violet serve -conf %s`\n", confFile)
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
func listenAddressValidator(ans interface{}) error {
|
||||
if ansStr, ok := ans.(string); ok {
|
||||
// empty string means disable
|
||||
if ansStr == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// use ResolveTCPAddr to validate the input
|
||||
_, err := net.ResolveTCPAddr("tcp", ansStr)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type nilTransport struct{}
|
||||
|
||||
func (n *nilTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("not sure how you are sending a request")
|
||||
}
|
31
database/db.go
Normal file
31
database/db.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
68
database/domain.sql.go
Normal file
68
database/domain.sql.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// source: domain.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const addDomain = `-- name: AddDomain :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO domains (domain, active)
|
||||
VALUES (?, ?)
|
||||
`
|
||||
|
||||
type AddDomainParams struct {
|
||||
Domain string `json:"domain"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddDomain(ctx context.Context, arg AddDomainParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addDomain, arg.Domain, arg.Active)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteDomain = `-- name: DeleteDomain :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO domains(domain, active)
|
||||
VALUES (?, false)
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteDomain(ctx context.Context, domain string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteDomain, domain)
|
||||
return err
|
||||
}
|
||||
|
||||
const getActiveDomains = `-- name: GetActiveDomains :many
|
||||
SELECT domain
|
||||
FROM domains
|
||||
WHERE active = 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveDomains(ctx context.Context) ([]string, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActiveDomains)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var domain string
|
||||
if err := rows.Scan(&domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, domain)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
74
database/favicon.sql.go
Normal file
74
database/favicon.sql.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// source: favicon.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const getFavicons = `-- name: GetFavicons :many
|
||||
SELECT host, svg, png, ico
|
||||
FROM favicons
|
||||
`
|
||||
|
||||
type GetFaviconsRow struct {
|
||||
Host string `json:"host"`
|
||||
Svg sql.NullString `json:"svg"`
|
||||
Png sql.NullString `json:"png"`
|
||||
Ico sql.NullString `json:"ico"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFavicons(ctx context.Context) ([]GetFaviconsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getFavicons)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetFaviconsRow
|
||||
for rows.Next() {
|
||||
var i GetFaviconsRow
|
||||
if err := rows.Scan(
|
||||
&i.Host,
|
||||
&i.Svg,
|
||||
&i.Png,
|
||||
&i.Ico,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateFaviconCache = `-- name: UpdateFaviconCache :exec
|
||||
INSERT OR
|
||||
REPLACE INTO favicons (host, svg, png, ico)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type UpdateFaviconCacheParams struct {
|
||||
Host string `json:"host"`
|
||||
Svg sql.NullString `json:"svg"`
|
||||
Png sql.NullString `json:"png"`
|
||||
Ico sql.NullString `json:"ico"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateFaviconCache(ctx context.Context, arg UpdateFaviconCacheParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateFaviconCache,
|
||||
arg.Host,
|
||||
arg.Svg,
|
||||
arg.Png,
|
||||
arg.Ico,
|
||||
)
|
||||
return err
|
||||
}
|
4
database/migrations/20240308125121_init.down.sql
Normal file
4
database/migrations/20240308125121_init.down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
DROP TABLE domains;
|
||||
DROP TABLE favicons;
|
||||
DROP TABLE routes;
|
||||
DROP TABLE redirects;
|
36
database/migrations/20240308125121_init.up.sql
Normal file
36
database/migrations/20240308125121_init.up.sql
Normal file
@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS domains
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT UNIQUE NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS favicons
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host VARCHAR NOT NULL,
|
||||
svg VARCHAR,
|
||||
png VARCHAR,
|
||||
ico VARCHAR
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS routes
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT UNIQUE NOT NULL,
|
||||
destination TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
flags INTEGER NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS redirects
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT UNIQUE NOT NULL,
|
||||
destination TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
flags INTEGER NOT NULL DEFAULT 0,
|
||||
code INTEGER NOT NULL DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT 1
|
||||
);
|
44
database/models.go
Normal file
44
database/models.go
Normal file
@ -0,0 +1,44 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/1f349/violet/target"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
ID int64 `json:"id"`
|
||||
Domain string `json:"domain"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type Favicon struct {
|
||||
ID int64 `json:"id"`
|
||||
Host string `json:"host"`
|
||||
Svg sql.NullString `json:"svg"`
|
||||
Png sql.NullString `json:"png"`
|
||||
Ico sql.NullString `json:"ico"`
|
||||
}
|
||||
|
||||
type Redirect struct {
|
||||
ID int64 `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Code int64 `json:"code"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
type Route struct {
|
||||
ID int64 `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Active bool `json:"active"`
|
||||
}
|
16
database/queries/domain.sql
Normal file
16
database/queries/domain.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- name: GetActiveDomains :many
|
||||
SELECT domain
|
||||
FROM domains
|
||||
WHERE active = 1;
|
||||
|
||||
-- name: AddDomain :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO domains (domain, active)
|
||||
VALUES (?, ?);
|
||||
|
||||
-- name: DeleteDomain :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO domains(domain, active)
|
||||
VALUES (?, false);
|
8
database/queries/favicon.sql
Normal file
8
database/queries/favicon.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- name: GetFavicons :many
|
||||
SELECT host, svg, png, ico
|
||||
FROM favicons;
|
||||
|
||||
-- name: UpdateFaviconCache :exec
|
||||
INSERT OR
|
||||
REPLACE INTO favicons (host, svg, png, ico)
|
||||
VALUES (?, ?, ?, ?);
|
39
database/queries/routing.sql
Normal file
39
database/queries/routing.sql
Normal file
@ -0,0 +1,39 @@
|
||||
-- name: GetActiveRoutes :many
|
||||
SELECT source, destination, flags
|
||||
FROM routes
|
||||
WHERE active = 1;
|
||||
|
||||
-- name: GetActiveRedirects :many
|
||||
SELECT source, destination, flags, code
|
||||
FROM redirects
|
||||
WHERE active = 1;
|
||||
|
||||
-- name: GetAllRoutes :many
|
||||
SELECT source, destination, description, flags, active
|
||||
FROM routes;
|
||||
|
||||
-- name: GetAllRedirects :many
|
||||
SELECT source, destination, description, flags, code, active
|
||||
FROM redirects;
|
||||
|
||||
-- name: AddRoute :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO routes (source, destination, description, flags, active)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
-- name: AddRedirect :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO redirects (source, destination, description, flags, code, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?);
|
||||
|
||||
-- name: RemoveRoute :exec
|
||||
DELETE
|
||||
FROM routes
|
||||
WHERE source = ?;
|
||||
|
||||
-- name: RemoveRedirect :exec
|
||||
DELETE
|
||||
FROM redirects
|
||||
WHERE source = ?;
|
250
database/routing.sql.go
Normal file
250
database/routing.sql.go
Normal file
@ -0,0 +1,250 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// source: routing.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1f349/violet/target"
|
||||
)
|
||||
|
||||
const addRedirect = `-- name: AddRedirect :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO redirects (source, destination, description, flags, code, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type AddRedirectParams struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Code int64 `json:"code"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddRedirect(ctx context.Context, arg AddRedirectParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addRedirect,
|
||||
arg.Source,
|
||||
arg.Destination,
|
||||
arg.Description,
|
||||
arg.Flags,
|
||||
arg.Code,
|
||||
arg.Active,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const addRoute = `-- name: AddRoute :exec
|
||||
INSERT OR
|
||||
REPLACE
|
||||
INTO routes (source, destination, description, flags, active)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
type AddRouteParams struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) AddRoute(ctx context.Context, arg AddRouteParams) error {
|
||||
_, err := q.db.ExecContext(ctx, addRoute,
|
||||
arg.Source,
|
||||
arg.Destination,
|
||||
arg.Description,
|
||||
arg.Flags,
|
||||
arg.Active,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const getActiveRedirects = `-- name: GetActiveRedirects :many
|
||||
SELECT source, destination, flags, code
|
||||
FROM redirects
|
||||
WHERE active = 1
|
||||
`
|
||||
|
||||
type GetActiveRedirectsRow struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Code int64 `json:"code"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetActiveRedirects(ctx context.Context) ([]GetActiveRedirectsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActiveRedirects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetActiveRedirectsRow
|
||||
for rows.Next() {
|
||||
var i GetActiveRedirectsRow
|
||||
if err := rows.Scan(
|
||||
&i.Source,
|
||||
&i.Destination,
|
||||
&i.Flags,
|
||||
&i.Code,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getActiveRoutes = `-- name: GetActiveRoutes :many
|
||||
SELECT source, destination, flags
|
||||
FROM routes
|
||||
WHERE active = 1
|
||||
`
|
||||
|
||||
type GetActiveRoutesRow struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetActiveRoutes(ctx context.Context) ([]GetActiveRoutesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getActiveRoutes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetActiveRoutesRow
|
||||
for rows.Next() {
|
||||
var i GetActiveRoutesRow
|
||||
if err := rows.Scan(&i.Source, &i.Destination, &i.Flags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAllRedirects = `-- name: GetAllRedirects :many
|
||||
SELECT source, destination, description, flags, code, active
|
||||
FROM redirects
|
||||
`
|
||||
|
||||
type GetAllRedirectsRow struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Code int64 `json:"code"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllRedirects(ctx context.Context) ([]GetAllRedirectsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllRedirects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllRedirectsRow
|
||||
for rows.Next() {
|
||||
var i GetAllRedirectsRow
|
||||
if err := rows.Scan(
|
||||
&i.Source,
|
||||
&i.Destination,
|
||||
&i.Description,
|
||||
&i.Flags,
|
||||
&i.Code,
|
||||
&i.Active,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAllRoutes = `-- name: GetAllRoutes :many
|
||||
SELECT source, destination, description, flags, active
|
||||
FROM routes
|
||||
`
|
||||
|
||||
type GetAllRoutesRow struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Description string `json:"description"`
|
||||
Flags target.Flags `json:"flags"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllRoutes(ctx context.Context) ([]GetAllRoutesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllRoutes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllRoutesRow
|
||||
for rows.Next() {
|
||||
var i GetAllRoutesRow
|
||||
if err := rows.Scan(
|
||||
&i.Source,
|
||||
&i.Destination,
|
||||
&i.Description,
|
||||
&i.Flags,
|
||||
&i.Active,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeRedirect = `-- name: RemoveRedirect :exec
|
||||
DELETE
|
||||
FROM redirects
|
||||
WHERE source = ?
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveRedirect(ctx context.Context, source string) error {
|
||||
_, err := q.db.ExecContext(ctx, removeRedirect, source)
|
||||
return err
|
||||
}
|
||||
|
||||
const removeRoute = `-- name: RemoveRoute :exec
|
||||
DELETE
|
||||
FROM routes
|
||||
WHERE source = ?
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveRoute(ctx context.Context, source string) error {
|
||||
_, err := q.db.ExecContext(ctx, removeRoute, source)
|
||||
return err
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS domains
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT,
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
@ -1,41 +1,34 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"log"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed create-table-domains.sql
|
||||
var createTableDomains string
|
||||
var Logger = logger.Logger.WithPrefix("Violet Domains")
|
||||
|
||||
// Domains is the domain list and management system.
|
||||
type Domains struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
s *sync.RWMutex
|
||||
m map[string]struct{}
|
||||
r *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
// New creates a new domain list
|
||||
func New(db *sql.DB) *Domains {
|
||||
func New(db *database.Queries) *Domains {
|
||||
a := &Domains{
|
||||
db: db,
|
||||
s: &sync.RWMutex{},
|
||||
m: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
// init domains table
|
||||
_, err := a.db.Exec(createTableDomains)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate 'domains' table\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// run compile to get the initial data
|
||||
a.Compile()
|
||||
a.r = rescheduler.NewRescheduler(a.threadCompile)
|
||||
return a
|
||||
}
|
||||
|
||||
@ -48,12 +41,15 @@ func (d *Domains) IsValid(host string) bool {
|
||||
defer d.s.RUnlock()
|
||||
|
||||
// check root domains `www.example.com`, `example.com`, `com`
|
||||
// TODO: could be faster using indexes and cropping the string?
|
||||
n := strings.Split(domain, ".")
|
||||
for i := 0; i < len(n); i++ {
|
||||
if _, ok := d.m[strings.Join(n[i:], ".")]; ok {
|
||||
for len(domain) > 0 {
|
||||
if _, ok := d.m[domain]; ok {
|
||||
return true
|
||||
}
|
||||
n := strings.IndexByte(domain, '.')
|
||||
if n == -1 {
|
||||
break
|
||||
}
|
||||
domain = domain[n+1:]
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -61,49 +57,65 @@ func (d *Domains) IsValid(host string) bool {
|
||||
// Compile downloads the list of domains from the database and loads them into
|
||||
// memory for faster lookups.
|
||||
//
|
||||
// This method is asynchronous and uses locks for safety.
|
||||
// This method makes use of the rescheduler instead of just ignoring multiple
|
||||
// calls.
|
||||
func (d *Domains) Compile() {
|
||||
// async compile magic
|
||||
go func() {
|
||||
// new map
|
||||
domainMap := make(map[string]struct{})
|
||||
d.r.Run()
|
||||
}
|
||||
|
||||
// compile map and check errors
|
||||
err := d.internalCompile(domainMap)
|
||||
if err != nil {
|
||||
log.Printf("[Domains] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
func (d *Domains) threadCompile() {
|
||||
// new map
|
||||
domainMap := make(map[string]struct{})
|
||||
|
||||
// lock while replacing the map
|
||||
d.s.Lock()
|
||||
d.m = domainMap
|
||||
d.s.Unlock()
|
||||
}()
|
||||
// compile map and check errors
|
||||
err := d.internalCompile(domainMap)
|
||||
if err != nil {
|
||||
Logger.Info("Compile faile", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// lock while replacing the map
|
||||
d.s.Lock()
|
||||
d.m = domainMap
|
||||
d.s.Unlock()
|
||||
}
|
||||
|
||||
// internalCompile is a hidden internal method for querying the database during
|
||||
// the Compile() method.
|
||||
func (d *Domains) internalCompile(m map[string]struct{}) error {
|
||||
log.Println("[Domains] Updating domains from database")
|
||||
Logger.Info("Updating domains from database")
|
||||
|
||||
// sql or something?
|
||||
rows, err := d.db.Query(`select domain from domains where active = 1`)
|
||||
rows, err := d.db.GetActiveDomains(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// loop through rows and scan the allowed domain names
|
||||
for rows.Next() {
|
||||
var name string
|
||||
err = rows.Scan(&name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m[name] = struct{}{}
|
||||
for _, i := range rows {
|
||||
m[i] = struct{}{}
|
||||
}
|
||||
|
||||
// check for errors
|
||||
return rows.Err()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Domains) Put(domain string, active bool) {
|
||||
d.s.Lock()
|
||||
defer d.s.Unlock()
|
||||
err := d.db.AddDomain(context.Background(), database.AddDomainParams{
|
||||
Domain: domain,
|
||||
Active: active,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Database error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Domains) Delete(domain string) {
|
||||
d.s.Lock()
|
||||
defer d.s.Unlock()
|
||||
err := d.db.DeleteDomain(context.Background(), domain)
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Database error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
47
domains/domains_test.go
Normal file
47
domains/domains_test.go
Normal file
@ -0,0 +1,47 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainsNew(t *testing.T) {
|
||||
db, err := violet.InitDB("file:TestDomainsNew?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains := New(db)
|
||||
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true})
|
||||
assert.NoError(t, err)
|
||||
domains.Compile()
|
||||
|
||||
if _, ok := domains.m["example.com"]; ok {
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
if _, ok := domains.m["www.example.com"]; !ok {
|
||||
assert.False(t, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomains_IsValid(t *testing.T) {
|
||||
// open sqlite database
|
||||
db, err := violet.InitDB("file:TestDomains_IsValid?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains := New(db)
|
||||
err = db.AddDomain(context.Background(), database.AddDomainParams{Domain: "example.com", Active: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
domains.s.Lock()
|
||||
assert.NoError(t, domains.internalCompile(domains.m))
|
||||
domains.s.Unlock()
|
||||
|
||||
assert.True(t, domains.IsValid("example.com"))
|
||||
assert.True(t, domains.IsValid("www.example.com"))
|
||||
assert.False(t, domains.IsValid("notexample.com"))
|
||||
assert.False(t, domains.IsValid("www.notexample.com"))
|
||||
}
|
@ -2,8 +2,9 @@ package error_pages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -11,6 +12,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Error Pages")
|
||||
|
||||
// ErrorPages stores the custom error pages and is called by the servers to
|
||||
// output meaningful pages for HTTP error codes
|
||||
type ErrorPages struct {
|
||||
@ -18,17 +21,19 @@ type ErrorPages struct {
|
||||
m map[int]func(rw http.ResponseWriter)
|
||||
generic func(rw http.ResponseWriter, code int)
|
||||
dir fs.FS
|
||||
r *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
// New creates a new error pages generator
|
||||
func New(dir fs.FS) *ErrorPages {
|
||||
return &ErrorPages{
|
||||
e := &ErrorPages{
|
||||
s: &sync.RWMutex{},
|
||||
m: make(map[int]func(rw http.ResponseWriter)),
|
||||
// generic error page writer
|
||||
generic: func(rw http.ResponseWriter, code int) {
|
||||
// if status text is empty then the code is unknown
|
||||
a := http.StatusText(code)
|
||||
fmt.Printf("%d - %s\n", code, a)
|
||||
if a != "" {
|
||||
// output in "xxx Error Text" format
|
||||
http.Error(rw, fmt.Sprintf("%d %s\n", code, a), code)
|
||||
@ -39,6 +44,8 @@ func New(dir fs.FS) *ErrorPages {
|
||||
},
|
||||
dir: dir,
|
||||
}
|
||||
e.r = rescheduler.NewRescheduler(e.threadCompile)
|
||||
return e
|
||||
}
|
||||
|
||||
// ServeError writes the error page for the given code to the response writer
|
||||
@ -57,34 +64,41 @@ func (e *ErrorPages) ServeError(rw http.ResponseWriter, code int) {
|
||||
e.generic(rw, code)
|
||||
}
|
||||
|
||||
// Compile loads the error pages the certificates and keys from the directories.
|
||||
//
|
||||
// This method makes use of the rescheduler instead of just ignoring multiple
|
||||
// calls.
|
||||
func (e *ErrorPages) Compile() {
|
||||
// async compile magic
|
||||
go func() {
|
||||
// new map
|
||||
errorPageMap := make(map[int]func(rw http.ResponseWriter))
|
||||
e.r.Run()
|
||||
}
|
||||
|
||||
// compile map and check errors
|
||||
func (e *ErrorPages) threadCompile() {
|
||||
// new map
|
||||
errorPageMap := make(map[int]func(rw http.ResponseWriter))
|
||||
|
||||
// compile map and check errors
|
||||
if e.dir != nil {
|
||||
err := e.internalCompile(errorPageMap)
|
||||
if err != nil {
|
||||
log.Printf("[Certs] Compile failed: %s\n", err)
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// lock while replacing the map
|
||||
e.s.Lock()
|
||||
e.m = errorPageMap
|
||||
e.s.Unlock()
|
||||
}()
|
||||
// lock while replacing the map
|
||||
e.s.Lock()
|
||||
e.m = errorPageMap
|
||||
e.s.Unlock()
|
||||
}
|
||||
|
||||
func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) error {
|
||||
// try to read dir
|
||||
files, err := fs.ReadDir(e.dir, "")
|
||||
files, err := fs.ReadDir(e.dir, ".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read error pages dir: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[ErrorPages] Compiling lookup table for %d error pages\n", len(files))
|
||||
Logger.Info("Compiling lookup table", "page count", len(files))
|
||||
|
||||
// find and load error pages
|
||||
for _, i := range files {
|
||||
@ -98,21 +112,21 @@ func (e *ErrorPages) internalCompile(m map[int]func(rw http.ResponseWriter)) err
|
||||
ext := filepath.Ext(name)
|
||||
|
||||
// if the extension is not 'html' then ignore the file
|
||||
if ext != "html" {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring non '.html' file in error pages directory: '%s'\n", name)
|
||||
if ext != ".html" {
|
||||
Logger.Warn("Ignoring non '.html' file in error pages directory", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// if the name can't be
|
||||
nameInt, err := strconv.Atoi(strings.TrimSuffix(name, ".html"))
|
||||
if err != nil {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory: '%s'\n", name)
|
||||
Logger.Warn("Ignoring invalid error page in error pages directory", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// check if code is in range 100-599
|
||||
if nameInt < 100 || nameInt >= 600 {
|
||||
log.Printf("[ErrorPages] WARNING: ignoring invalid error page in error pages directory must be 100-599: '%s'\n", name)
|
||||
Logger.Warn("Ignoring invalid error page in error pages directory must be 100-599", "name", name)
|
||||
continue
|
||||
}
|
||||
|
||||
|
64
error-pages/error-pages_test.go
Normal file
64
error-pages/error-pages_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package error_pages
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
)
|
||||
|
||||
func TestErrorPages_ServeError(t *testing.T) {
|
||||
errorPages := New(nil)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
errorPages.ServeError(rec, http.StatusTeapot)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Equal(t, "418 I'm a teapot", res.Status)
|
||||
a, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "418 I'm a teapot\n\n", string(a))
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
errorPages.ServeError(rec, 469)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, 469, res.StatusCode)
|
||||
assert.Equal(t, "469 ", res.Status)
|
||||
a, err = io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "469 Unknown Error Code\n\n", string(a))
|
||||
}
|
||||
|
||||
func TestErrorPagesWithCustom(t *testing.T) {
|
||||
fs := fstest.MapFS{
|
||||
"418.html": {
|
||||
Data: []byte("418 Custom Error Page\n"),
|
||||
},
|
||||
"469.html": {
|
||||
Data: []byte("469 Custom Error Page\n"),
|
||||
},
|
||||
}
|
||||
|
||||
errorPages := New(fs)
|
||||
assert.NoError(t, errorPages.internalCompile(errorPages.m))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
errorPages.ServeError(rec, http.StatusTeapot)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Equal(t, "418 I'm a teapot", res.Status)
|
||||
a, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "418 Custom Error Page\n", string(a))
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
errorPages.ServeError(rec, 469)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, 469, res.StatusCode)
|
||||
assert.Equal(t, "469 ", res.Status)
|
||||
a, err = io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "469 Custom Error Page\n", string(a))
|
||||
}
|
BIN
favicons/example.ico
Normal file
BIN
favicons/example.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
favicons/example.png
Normal file
BIN
favicons/example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
4
favicons/example.svg
Normal file
4
favicons/example.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg height="100" width="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"/>
|
||||
</svg>
|
After Width: | Height: | Size: 214 B |
19
favicons/favicon-image.go
Normal file
19
favicons/favicon-image.go
Normal file
@ -0,0 +1,19 @@
|
||||
package favicons
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// FaviconImage stores the url, hash and raw bytes of an image
|
||||
type FaviconImage struct {
|
||||
Url string
|
||||
Hash string
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// CreateFaviconImage outputs a FaviconImage with the specified URL or nil if
|
||||
// the URL is an empty string.
|
||||
func CreateFaviconImage(url sql.NullString) *FaviconImage {
|
||||
if !url.Valid {
|
||||
return nil
|
||||
}
|
||||
return &FaviconImage{Url: url.String}
|
||||
}
|
166
favicons/favicon-list.go
Normal file
166
favicons/favicon-list.go
Normal file
@ -0,0 +1,166 @@
|
||||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mrmelon54/png2ico"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FaviconList contains the ico, png and svg icons for separate favicons
|
||||
type FaviconList struct {
|
||||
Ico *FaviconImage // can be generated from png with wrapper
|
||||
Png *FaviconImage // can be generated from svg with inkscape
|
||||
Svg *FaviconImage
|
||||
}
|
||||
|
||||
var ErrInvalidFaviconExtension = errors.New("invalid favicon extension")
|
||||
|
||||
// ProduceForExt outputs the bytes for the ico/png/svg icon and the HTTP
|
||||
// Content-Type header to output.
|
||||
func (l *FaviconList) ProduceForExt(ext string) (raw []byte, contentType string, err error) {
|
||||
switch ext {
|
||||
case ".ico":
|
||||
contentType = "image/x-icon"
|
||||
raw, err = l.ProduceIco()
|
||||
case ".png":
|
||||
contentType = "image/png"
|
||||
raw, err = l.ProducePng()
|
||||
case ".svg":
|
||||
contentType = "image/svg+xml"
|
||||
raw, err = l.ProduceSvg()
|
||||
default:
|
||||
err = ErrInvalidFaviconExtension
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProduceIco outputs the bytes of the ico icon or an error
|
||||
func (l *FaviconList) ProduceIco() ([]byte, error) {
|
||||
if l.Ico == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Ico.Raw, nil
|
||||
}
|
||||
|
||||
// ProducePng outputs the bytes of the png icon or an error
|
||||
func (l *FaviconList) ProducePng() ([]byte, error) {
|
||||
if l.Png == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Png.Raw, nil
|
||||
}
|
||||
|
||||
// ProduceSvg outputs the bytes of the svg icon or an error
|
||||
func (l *FaviconList) ProduceSvg() ([]byte, error) {
|
||||
if l.Svg == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Svg.Raw, nil
|
||||
}
|
||||
|
||||
// PreProcess takes an input of the svg2png conversion function and outputs
|
||||
// an error if the SVG, PNG or ICO fails to download or generate
|
||||
func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error {
|
||||
var err error
|
||||
|
||||
// SVG
|
||||
if l.Svg != nil {
|
||||
// download SVG
|
||||
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to fetch SVG icon: %w", err)
|
||||
}
|
||||
l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw))
|
||||
}
|
||||
|
||||
// PNG
|
||||
if l.Png != nil {
|
||||
// download PNG
|
||||
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to fetch PNG icon: %w", err)
|
||||
}
|
||||
} else if l.Svg != nil {
|
||||
// generate PNG from SVG
|
||||
l.Png = &FaviconImage{}
|
||||
l.Png.Raw, err = convert(l.Svg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to generate PNG icon: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ICO
|
||||
if l.Ico != nil {
|
||||
// download ICO
|
||||
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to fetch ICO icon: %w", err)
|
||||
}
|
||||
} else if l.Png != nil {
|
||||
// generate ICO from PNG
|
||||
l.Ico = &FaviconImage{}
|
||||
decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to decode PNG icon: %w", err)
|
||||
}
|
||||
b := decode.Bounds()
|
||||
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
|
||||
if err != nil {
|
||||
return fmt.Errorf("favicons: failed to generate ICO icon: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// generate sha256 hashes for svg, png and ico
|
||||
l.genSha256()
|
||||
return nil
|
||||
}
|
||||
|
||||
// genSha256 generates sha256 hashes
|
||||
func (l *FaviconList) genSha256() {
|
||||
if l.Svg != nil {
|
||||
l.Svg.Hash = genSha256(l.Svg.Raw)
|
||||
}
|
||||
if l.Png != nil {
|
||||
l.Png.Hash = genSha256(l.Png.Raw)
|
||||
}
|
||||
if l.Ico != nil {
|
||||
l.Ico.Hash = genSha256(l.Ico.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// getFaviconViaRequest uses the standard http request library to download
|
||||
// icons, outputs the raw bytes from the download or an error.
|
||||
var getFaviconViaRequest = func(url string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("favicons: Failed to send request '%s': %w", url, err)
|
||||
}
|
||||
req.Header.Set("X-Violet-Raw-Favicon", "1")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("favicons: failed to do request '%s': %w", url, err)
|
||||
}
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("favicons: failed to read response '%s': %w", url, err)
|
||||
}
|
||||
return rawBody, nil
|
||||
}
|
||||
|
||||
// genSha256 generates a sha256 hash as a hex encoded string
|
||||
func genSha256(in []byte) string {
|
||||
// create sha256 generator and write to it
|
||||
h := sha256.New()
|
||||
_, err := h.Write(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// encode as hex
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
28
favicons/favicon-list_test.go
Normal file
28
favicons/favicon-list_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"image/png"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFaviconList_PreProcess(t *testing.T) {
|
||||
getFaviconViaRequest = func(_ string) ([]byte, error) {
|
||||
return exampleSvg, nil
|
||||
}
|
||||
icons := &FaviconList{Svg: &FaviconImage{Url: "https://example.com/assets/logo.svg"}}
|
||||
assert.NoError(t, icons.PreProcess(func(in []byte) ([]byte, error) {
|
||||
return svg2png("inkscape", in)
|
||||
}))
|
||||
assert.Equal(t, "https://example.com/assets/logo.svg", icons.Svg.Url)
|
||||
|
||||
assert.Equal(t, "74cdc17d0502a690941799c327d9ca1ed042e76c784def43a42937f2eed270b4", icons.Svg.Hash)
|
||||
assert.NotEqual(t, "", icons.Png.Hash)
|
||||
assert.NotEqual(t, "", icons.Ico.Hash)
|
||||
|
||||
// verify png bytes are a valid png image
|
||||
pngRaw := bytes.NewBuffer(icons.Png.Raw)
|
||||
_, err := png.Decode(pngRaw)
|
||||
assert.NoError(t, err)
|
||||
}
|
@ -1,75 +1,45 @@
|
||||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mrmelon54/png2ico"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Favicons")
|
||||
|
||||
var ErrFaviconNotFound = errors.New("favicon not found")
|
||||
|
||||
// Favicons is a dynamic favicon generator which supports overwriting favicons
|
||||
type Favicons struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
cmd string
|
||||
cLock *sync.RWMutex
|
||||
faviconMap map[string]*FaviconList
|
||||
r *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
// New creates a new dynamic favicon generator
|
||||
func New(db *sql.DB, inkscapeCmd string) *Favicons {
|
||||
func New(db *database.Queries, inkscapeCmd string) *Favicons {
|
||||
f := &Favicons{
|
||||
db: db,
|
||||
cmd: inkscapeCmd,
|
||||
cLock: &sync.RWMutex{},
|
||||
faviconMap: make(map[string]*FaviconList),
|
||||
}
|
||||
|
||||
// init favicons table
|
||||
_, err := f.db.Exec(`create table if not exists favicons (id integer primary key autoincrement, host varchar, svg varchar, png varchar, ico varchar)`)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate 'favicons' table\n")
|
||||
return nil
|
||||
}
|
||||
f.r = rescheduler.NewRescheduler(f.threadCompile)
|
||||
|
||||
// run compile to get the initial data
|
||||
f.Compile()
|
||||
return f
|
||||
}
|
||||
|
||||
// Compile downloads the list of favicon mappings from the database and loads
|
||||
// them and the target favicons into memory for faster lookups
|
||||
func (f *Favicons) Compile() {
|
||||
// async compile magic
|
||||
go func() {
|
||||
// new map
|
||||
favicons := make(map[string]*FaviconList)
|
||||
|
||||
// compile map and check errors
|
||||
err := f.internalCompile(favicons)
|
||||
if err != nil {
|
||||
// log compile errors
|
||||
log.Printf("[Favicons] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// lock while replacing the map
|
||||
f.cLock.Lock()
|
||||
f.faviconMap = favicons
|
||||
f.cLock.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// GetIcons returns the favicon list for the provided host or nil if no
|
||||
// icon is found or generated
|
||||
func (f *Favicons) GetIcons(host string) *FaviconList {
|
||||
@ -81,33 +51,54 @@ func (f *Favicons) GetIcons(host string) *FaviconList {
|
||||
return f.faviconMap[host]
|
||||
}
|
||||
|
||||
// Compile downloads the list of favicon mappings from the database and loads
|
||||
// them and the target favicons into memory for faster lookups
|
||||
//
|
||||
// This method makes use of the rescheduler instead of just ignoring multiple
|
||||
// calls.
|
||||
func (f *Favicons) Compile() {
|
||||
f.r.Run()
|
||||
}
|
||||
|
||||
func (f *Favicons) threadCompile() {
|
||||
// new map
|
||||
favicons := make(map[string]*FaviconList)
|
||||
|
||||
// compile map and check errors
|
||||
err := f.internalCompile(favicons)
|
||||
if err != nil {
|
||||
// log compile errors
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// lock while replacing the map
|
||||
f.cLock.Lock()
|
||||
f.faviconMap = favicons
|
||||
f.cLock.Unlock()
|
||||
}
|
||||
|
||||
// internalCompile is a hidden internal method for loading and generating all
|
||||
// favicons.
|
||||
func (f *Favicons) internalCompile(faviconMap map[string]*FaviconList) error {
|
||||
func (f *Favicons) internalCompile(m map[string]*FaviconList) error {
|
||||
// query all rows in database
|
||||
query, err := f.db.Query(`select host, svg, png, ico from favicons`)
|
||||
rows, err := f.db.GetFavicons(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare query: %w", err)
|
||||
return fmt.Errorf("failed to prepare rows: %w", err)
|
||||
}
|
||||
|
||||
// loop over rows and scan in data using error group to catch errors
|
||||
var g errgroup.Group
|
||||
for query.Next() {
|
||||
var host, rawSvg, rawPng, rawIco string
|
||||
err := query.Scan(&host, &rawSvg, &rawPng, &rawIco)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to scan row: %w", err)
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
// create favicon list for this row
|
||||
l := &FaviconList{
|
||||
Ico: CreateFaviconImage(rawIco),
|
||||
Png: CreateFaviconImage(rawPng),
|
||||
Svg: CreateFaviconImage(rawSvg),
|
||||
Ico: CreateFaviconImage(row.Ico),
|
||||
Png: CreateFaviconImage(row.Png),
|
||||
Svg: CreateFaviconImage(row.Svg),
|
||||
}
|
||||
|
||||
// save the favicon list to the map
|
||||
faviconMap[host] = l
|
||||
m[row.Host] = l
|
||||
|
||||
// run the pre-process in a separate goroutine
|
||||
g.Go(func() error {
|
||||
@ -121,150 +112,3 @@ func (f *Favicons) internalCompile(faviconMap map[string]*FaviconList) error {
|
||||
func (f *Favicons) convertSvgToPng(in []byte) ([]byte, error) {
|
||||
return svg2png(f.cmd, in)
|
||||
}
|
||||
|
||||
// FaviconList contains the ico, png and svg icons for separate favicons
|
||||
type FaviconList struct {
|
||||
Ico *FaviconImage // can be generated from png with wrapper
|
||||
Png *FaviconImage // can be generated from svg with inkscape
|
||||
Svg *FaviconImage
|
||||
}
|
||||
|
||||
// ProduceIco outputs the bytes of the ico icon or an error
|
||||
func (l *FaviconList) ProduceIco() ([]byte, error) {
|
||||
if l.Ico == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Ico.Raw, nil
|
||||
}
|
||||
|
||||
// ProducePng outputs the bytes of the png icon or an error
|
||||
func (l *FaviconList) ProducePng() ([]byte, error) {
|
||||
if l.Png == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Png.Raw, nil
|
||||
}
|
||||
|
||||
// ProduceSvg outputs the bytes of the svg icon or an error
|
||||
func (l *FaviconList) ProduceSvg() ([]byte, error) {
|
||||
if l.Svg == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Svg.Raw, nil
|
||||
}
|
||||
|
||||
// PreProcess takes an input of the svg2png conversion function and outputs
|
||||
// an error if the SVG, PNG or ICO fails to download or generate
|
||||
func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error {
|
||||
var err error
|
||||
|
||||
// SVG
|
||||
if l.Svg != nil {
|
||||
// download SVG
|
||||
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch SVG icon: %w", err)
|
||||
}
|
||||
l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw))
|
||||
}
|
||||
|
||||
// PNG
|
||||
if l.Png != nil {
|
||||
// download PNG
|
||||
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch PNG icon: %w", err)
|
||||
}
|
||||
} else if l.Svg != nil {
|
||||
// generate PNG from SVG
|
||||
l.Png = &FaviconImage{}
|
||||
l.Png.Raw, err = convert(l.Svg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to generate PNG icon: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ICO
|
||||
if l.Ico != nil {
|
||||
// download ICO
|
||||
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch ICO icon: %w", err)
|
||||
}
|
||||
} else if l.Png != nil {
|
||||
// generate ICO from PNG
|
||||
l.Ico = &FaviconImage{}
|
||||
decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to decode PNG icon: %w", err)
|
||||
}
|
||||
b := decode.Bounds()
|
||||
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to generate ICO icon: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// generate sha256 hashes for svg, png and ico
|
||||
l.genSha256()
|
||||
return nil
|
||||
}
|
||||
|
||||
// genSha256 generates sha256 hashes
|
||||
func (l *FaviconList) genSha256() {
|
||||
if l.Svg != nil {
|
||||
l.Svg.Hash = genSha256(l.Svg.Raw)
|
||||
}
|
||||
if l.Png != nil {
|
||||
l.Png.Hash = genSha256(l.Png.Raw)
|
||||
}
|
||||
if l.Ico != nil {
|
||||
l.Ico.Hash = genSha256(l.Ico.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// getFaviconViaRequest uses the standard http request library to download
|
||||
// icons, outputs the raw bytes from the download or an error.
|
||||
func getFaviconViaRequest(url string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to send request '%s': %w", url, err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to do request '%s': %w", url, err)
|
||||
}
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to read response '%s': %w", url, err)
|
||||
}
|
||||
return rawBody, nil
|
||||
}
|
||||
|
||||
// genSha256 generates a sha256 hash as a hex encoded string
|
||||
func genSha256(in []byte) string {
|
||||
// create sha256 generator and write to it
|
||||
h := sha256.New()
|
||||
_, err := h.Write(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// encode as hex
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// FaviconImage stores the url, hash and raw bytes of an image
|
||||
type FaviconImage struct {
|
||||
Url string
|
||||
Hash string
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
// CreateFaviconImage outputs a FaviconImage with the specified URL or nil if
|
||||
// the URL is an empty string.
|
||||
func CreateFaviconImage(url string) *FaviconImage {
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
return &FaviconImage{Url: url}
|
||||
}
|
||||
|
55
favicons/favicons_test.go
Normal file
55
favicons/favicons_test.go
Normal file
@ -0,0 +1,55 @@
|
||||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"image/png"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed example.svg
|
||||
exampleSvg []byte
|
||||
//go:embed example.png
|
||||
examplePng []byte
|
||||
//go:embed example.ico
|
||||
exampleIco []byte
|
||||
)
|
||||
|
||||
func TestFaviconsNew(t *testing.T) {
|
||||
getFaviconViaRequest = func(_ string) ([]byte, error) { return exampleSvg, nil }
|
||||
|
||||
db, err := violet.InitDB("file:TestFaviconsNew?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
favicons := New(db, "inkscape")
|
||||
err = db.UpdateFaviconCache(context.Background(), database.UpdateFaviconCacheParams{
|
||||
Host: "example.com",
|
||||
Svg: sql.NullString{
|
||||
String: "https://example.com/assets/logo.svg",
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
favicons.cLock.Lock()
|
||||
assert.NoError(t, favicons.internalCompile(favicons.faviconMap))
|
||||
favicons.cLock.Unlock()
|
||||
|
||||
icons := favicons.GetIcons("example.com")
|
||||
assert.Equal(t, "https://example.com/assets/logo.svg", icons.Svg.Url)
|
||||
|
||||
assert.Equal(t, "74cdc17d0502a690941799c327d9ca1ed042e76c784def43a42937f2eed270b4", icons.Svg.Hash)
|
||||
assert.NotEqual(t, "", icons.Png.Hash)
|
||||
assert.NotEqual(t, "", icons.Ico.Hash)
|
||||
|
||||
// verify png bytes are a valid png image
|
||||
pngRaw := bytes.NewBuffer(icons.Png.Raw)
|
||||
_, err = png.Decode(pngRaw)
|
||||
assert.NoError(t, err)
|
||||
}
|
71
go.mod
71
go.mod
@ -1,35 +1,62 @@
|
||||
module github.com/MrMelon54/violet
|
||||
module github.com/1f349/violet
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1
|
||||
code.mrmelon54.com/melon/summer-utils v0.0.3
|
||||
github.com/MrMelon54/trie v0.0.2
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/1f349/mjwt v0.2.5
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/cloudflare/tableflip v1.2.3
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/mrmelon54/mjwt v0.0.1
|
||||
github.com/mrmelon54/png2ico v1.0.0
|
||||
github.com/rs/cors v1.9.0
|
||||
github.com/sethvargo/go-limiter v0.7.2
|
||||
github.com/stretchr/testify v1.8.2
|
||||
golang.org/x/net v0.9.0
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
xorm.io/xorm v1.3.2
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mrmelon54/certgen v0.0.2
|
||||
github.com/mrmelon54/png2ico v1.0.2
|
||||
github.com/mrmelon54/rescheduler v0.0.3
|
||||
github.com/mrmelon54/trie v0.0.3
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/sethvargo/go-limiter v1.0.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sync v0.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.10.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/goccy/go-json v0.8.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.14.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect
|
||||
)
|
||||
|
756
go.sum
756
go.sum
@ -1,666 +1,162 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1 h1:tll8DwvO1CL+xXJIMLyDmQYoYr/gA4BkcUFtNHB1BFo=
|
||||
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1/go.mod h1:Liyhe1bkNyeVfw6LicCgrQ+4oUT/w/qONLjvejkUim0=
|
||||
code.mrmelon54.com/melon/summer-utils v0.0.3 h1:Bz4o5BBOqWCNGpKkxUum4rwMn/DIdyMCKGQ/D6SXD6Q=
|
||||
code.mrmelon54.com/melon/summer-utils v0.0.3/go.mod h1:Gh/baXSzkf1ZhHonpPP8oQkyhhmFZcC2yTMlrwclDUw=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/MrMelon54/trie v0.0.2 h1:ZXWcX5ij62O9K4I/anuHmVg8L3tF0UGdlPceAASwKEY=
|
||||
github.com/MrMelon54/trie v0.0.2/go.mod h1:sGCGOcqb+DxSxvHgSOpbpkmA7mFZR47YDExy9OCbVZI=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/1f349/mjwt v0.2.5 h1:IxjLaali22ayTzZ628lH7j0JDdYJoj6+CJ/VktCqtXQ=
|
||||
github.com/1f349/mjwt v0.2.5/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
|
||||
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
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/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/cloudflare/tableflip v1.2.3 h1:8I+B99QnnEWPHOY3fWipwVKxS70LGgUsslG7CSfmHMw=
|
||||
github.com/cloudflare/tableflip v1.2.3/go.mod h1:P4gRehmV6Z2bY5ao5ml9Pd8u6kuEnlB37pUFMmv7j2E=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/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.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
|
||||
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
|
||||
github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE=
|
||||
github.com/jackc/pgtype v1.8.0/go.mod h1:PqDKcEBtllAtk/2p6z6SHdXW5UB+MhE75tUol2OKexE=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
|
||||
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
|
||||
github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc=
|
||||
github.com/jackc/pgx/v4 v4.12.0/go.mod h1:fE547h6VulLPA3kySjfnSG/e2D861g/50JlVUa/ub60=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mrmelon54/mjwt v0.0.1 h1:XgyWviTmgsbMiKXjxo+Jp/QSf7FF7/omkvrUag8/P5U=
|
||||
github.com/mrmelon54/mjwt v0.0.1/go.mod h1:M+kZ6t9EArEQ2/CGjfgyNhAo542ot+S7gw5uJCK11Ms=
|
||||
github.com/mrmelon54/png2ico v1.0.0 h1:YE20i0xao8rkuYaCq3Xj2hUkVkJ6xp412aGDMrGqufA=
|
||||
github.com/mrmelon54/png2ico v1.0.0/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
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/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY=
|
||||
github.com/mrmelon54/certgen v0.0.2/go.mod h1:vwrWSXQmxZYqEyh+cf05IvDIFV2aYuxL4+O6ABIlN8M=
|
||||
github.com/mrmelon54/png2ico v1.0.2 h1:KyJd3ATmDjxAJS28MTSf44GxzYnlZ+7KT8SXzGb3sN8=
|
||||
github.com/mrmelon54/png2ico v1.0.2/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
|
||||
github.com/mrmelon54/rescheduler v0.0.3 h1:TrkJL6S7PKvXuo1mvdgRgsILA/pk5L1lrXhV/q7IEzQ=
|
||||
github.com/mrmelon54/rescheduler v0.0.3/go.mod h1:q415n6W1xcePPP5Rix6FOiADgcN66BYMyNOsFnNyoWQ=
|
||||
github.com/mrmelon54/trie v0.0.3 h1:wZmws84FiGNBZJ00garLyQ2EQhtx0SipVoV7fK8+kZE=
|
||||
github.com/mrmelon54/trie v0.0.3/go.mod h1:d3hl0YUBSWR3XN4S9BDLkGVzLT4VgwP2mZkBJM6uFpw=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
|
||||
github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
|
||||
github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
|
||||
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
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/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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190125091013-d26f9f9a57f3/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/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-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
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.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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
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.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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/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=
|
||||
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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U=
|
||||
modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
|
||||
modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
|
||||
modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
|
||||
modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
|
||||
modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
|
||||
modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
|
||||
modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
|
||||
modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
|
||||
modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
|
||||
modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
|
||||
modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
|
||||
modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
|
||||
modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
|
||||
modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
|
||||
modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
|
||||
modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
|
||||
modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
|
||||
modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
|
||||
modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
|
||||
modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
|
||||
modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
|
||||
modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
|
||||
modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
|
||||
modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
|
||||
modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
|
||||
modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
|
||||
modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
|
||||
modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
|
||||
modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
|
||||
modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4=
|
||||
modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
|
||||
modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
|
||||
modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
|
||||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
|
||||
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
|
||||
modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
|
||||
modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
|
||||
modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
|
||||
modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
|
||||
modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
|
||||
modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
|
||||
modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
|
||||
modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
|
||||
modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
|
||||
modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
|
||||
modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
|
||||
modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
|
||||
modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
|
||||
modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
|
||||
modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
|
||||
modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
|
||||
modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
|
||||
modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
|
||||
modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
|
||||
modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
|
||||
modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
|
||||
modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
|
||||
modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
|
||||
modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
|
||||
modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
|
||||
modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
|
||||
modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
|
||||
modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
|
||||
modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
|
||||
modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
|
||||
modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE=
|
||||
modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
|
||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
|
||||
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
|
||||
modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
|
||||
modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
|
||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
|
||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
|
||||
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM=
|
||||
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.3.2 h1:uTRRKF2jYzbZ5nsofXVUx6ncMaek+SHjWYtCXyZo1oM=
|
||||
xorm.io/xorm v1.3.2/go.mod h1:9NbjqdnjX6eyjRRhh01GHm64r6N9shTb/8Ak3YRt8Nw=
|
||||
|
38
initdb.go
Normal file
38
initdb.go
Normal file
@ -0,0 +1,38 @@
|
||||
package violet
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
//go:embed database/migrations/*.sql
|
||||
var migrations embed.FS
|
||||
|
||||
func InitDB(p string) (*database.Queries, error) {
|
||||
migDrv, err := iofs.New(migrations, "database/migrations")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbOpen, err := sql.Open("sqlite3", p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbDrv, err := sqlite3.WithInstance(dbOpen, &sqlite3.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mig, err := migrate.NewWithInstance("iofs", migDrv, "sqlite3", dbDrv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = mig.Up()
|
||||
if err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||
return nil, err
|
||||
}
|
||||
return database.New(dbOpen), nil
|
||||
}
|
12
logger/logger.go
Normal file
12
logger/logger.go
Normal file
@ -0,0 +1,12 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Logger = log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportCaller: true,
|
||||
ReportTimestamp: true,
|
||||
Prefix: "Violet",
|
||||
})
|
88
proxy/hybrid-transport.go
Normal file
88
proxy/hybrid-transport.go
Normal file
@ -0,0 +1,88 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var loggerSecure = logger.Logger.WithPrefix("Violet Secure Transport")
|
||||
var loggerInsecure = logger.Logger.WithPrefix("Violet Insecure Transport")
|
||||
var loggerWebsocket = logger.Logger.WithPrefix("Violet Websocket Transport")
|
||||
|
||||
type HybridTransport struct {
|
||||
baseDialer *net.Dialer
|
||||
normalTransport http.RoundTripper
|
||||
insecureTransport http.RoundTripper
|
||||
socksSync *sync.RWMutex
|
||||
socksTransport map[string]http.RoundTripper
|
||||
ws *websocket.Server
|
||||
}
|
||||
|
||||
// NewHybridTransport creates a new hybrid transport
|
||||
func NewHybridTransport(ws *websocket.Server) *HybridTransport {
|
||||
return NewHybridTransportWithCalls(nil, nil, ws)
|
||||
}
|
||||
|
||||
// NewHybridTransportWithCalls creates new hybrid transport with custom normal
|
||||
// and insecure http.RoundTripper functions.
|
||||
//
|
||||
// NewHybridTransportWithCalls(nil, nil) is equivalent to NewHybridTransport()
|
||||
func NewHybridTransportWithCalls(normal, insecure http.RoundTripper, ws *websocket.Server) *HybridTransport {
|
||||
h := &HybridTransport{
|
||||
baseDialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
},
|
||||
normalTransport: normal,
|
||||
insecureTransport: insecure,
|
||||
ws: ws,
|
||||
}
|
||||
if h.normalTransport == nil {
|
||||
h.normalTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: h.baseDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 15,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
}
|
||||
if h.insecureTransport == nil {
|
||||
h.insecureTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: h.baseDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 15,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// SecureRoundTrip calls the secure transport
|
||||
func (h *HybridTransport) SecureRoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return h.normalTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// InsecureRoundTrip calls the insecure transport
|
||||
func (h *HybridTransport) InsecureRoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return h.insecureTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// ConnectWebsocket calls the websocket upgrader and thus hijacks the connection
|
||||
func (h *HybridTransport) ConnectWebsocket(rw http.ResponseWriter, req *http.Request) {
|
||||
h.ws.Upgrade(rw, req)
|
||||
}
|
16
proxy/hybrid-transport_test.go
Normal file
16
proxy/hybrid-transport_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewHybridTransport(t *testing.T) {
|
||||
h := NewHybridTransport(nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
trip, err := h.SecureRoundTrip(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, trip.StatusCode)
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"golang.org/x/net/proxy"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type reverseProxyHostKey int
|
||||
|
||||
type ReverseProxyContext interface {
|
||||
IsIgnoreCert() bool
|
||||
UpdateHeaders(http.Header)
|
||||
}
|
||||
|
||||
func SetReverseProxyHost(req *http.Request, hf ReverseProxyContext) *http.Request {
|
||||
ctx := req.Context()
|
||||
ctx2 := context.WithValue(ctx, reverseProxyHostKey(0), hf)
|
||||
return req.WithContext(ctx2)
|
||||
}
|
||||
|
||||
func CreateHybridReverseProxy() *httputil.ReverseProxy {
|
||||
return &httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {},
|
||||
Transport: NewHybridTransport(),
|
||||
ModifyResponse: func(rw *http.Response) error { return nil },
|
||||
ErrorHandler: func(rw http.ResponseWriter, req *http.Request, err error) {
|
||||
log.Printf("[ReverseProxy] Request: %#v\n -- Error: %s\n", req, err)
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = rw.Write([]byte("502 Bad gateway\n"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type HybridTransport struct {
|
||||
baseDialer *net.Dialer
|
||||
normalTransport http.RoundTripper
|
||||
insecureTransport http.RoundTripper
|
||||
socksSync *sync.RWMutex
|
||||
socksTransport map[string]http.RoundTripper
|
||||
}
|
||||
|
||||
func NewHybridTransport() *HybridTransport {
|
||||
h := &HybridTransport{
|
||||
baseDialer: &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
},
|
||||
socksSync: &sync.RWMutex{},
|
||||
socksTransport: make(map[string]http.RoundTripper),
|
||||
}
|
||||
h.normalTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: h.baseDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 15,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
h.insecureTransport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: h.baseDialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 15,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HybridTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
newHost := req.Context().Value(reverseProxyHostKey(0))
|
||||
hf, ok := newHost.(ReverseProxyContext)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to detect reverse proxy configuration")
|
||||
}
|
||||
|
||||
// Do a round trip using existing transports
|
||||
var trip *http.Response
|
||||
var err error
|
||||
if hf.IsIgnoreCert() {
|
||||
trip, err = h.insecureTransport.RoundTrip(req)
|
||||
} else {
|
||||
trip, err = h.normalTransport.RoundTrip(req)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Override headers
|
||||
hf.UpdateHeaders(trip.Header)
|
||||
return trip, nil
|
||||
}
|
||||
|
||||
func (h *HybridTransport) getSocksProxy(addr string, insecure bool) (http.RoundTripper, error) {
|
||||
if insecure {
|
||||
addr = "%i-" + addr
|
||||
}
|
||||
h.socksSync.RLock()
|
||||
s, ok := h.socksTransport[addr]
|
||||
h.socksSync.RUnlock()
|
||||
if ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
dialer, err := proxy.SOCKS5("tcp", addr, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot connect to the proxy: %s", err)
|
||||
}
|
||||
|
||||
if f, ok := dialer.(proxy.ContextDialer); ok {
|
||||
t := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: f.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 15,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if insecure {
|
||||
t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
h.socksSync.Lock()
|
||||
h.socksTransport[addr] = t
|
||||
h.socksSync.Unlock()
|
||||
return t, nil
|
||||
}
|
||||
return nil, errors.New("cannot create socks5 dialer")
|
||||
}
|
134
proxy/websocket/server.go
Normal file
134
proxy/websocket/server.go
Normal file
@ -0,0 +1,134 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Websocket")
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
HandshakeTimeout: time.Second * 5,
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// allow requests from any origin
|
||||
// the internal service can decide what origins to allow
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
connLock *sync.RWMutex
|
||||
connStop bool
|
||||
conns map[string]*websocket.Conn
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{
|
||||
connLock: new(sync.RWMutex),
|
||||
conns: make(map[string]*websocket.Conn),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Upgrade(rw http.ResponseWriter, req *http.Request) {
|
||||
req.URL.Scheme = "ws"
|
||||
Logger.Info("Upgrading request", "url", req.URL, "origin", req.Header.Get("Origin"))
|
||||
|
||||
c, err := upgrader.Upgrade(rw, req, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
s.connLock.Lock()
|
||||
|
||||
// no more connections allowed
|
||||
if s.connStop {
|
||||
s.connLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// save connection for shutdown
|
||||
s.conns[c.RemoteAddr().String()] = c
|
||||
s.connLock.Unlock()
|
||||
|
||||
Logger.Info("Dialing", "url", req.URL)
|
||||
|
||||
// dial for internal connection
|
||||
ic, _, err := websocket.DefaultDialer.DialContext(req.Context(), req.URL.String(), filterWebsocketHeaders(req.Header))
|
||||
if err != nil {
|
||||
Logger.Info("Failed to dial", "url", req.URL, "err", err)
|
||||
s.Remove(c)
|
||||
return
|
||||
}
|
||||
defer ic.Close()
|
||||
|
||||
d1 := make(chan struct{}, 1)
|
||||
d2 := make(chan struct{}, 1)
|
||||
|
||||
// relay messages each way
|
||||
go s.wsRelay(d1, c, ic)
|
||||
go s.wsRelay(d2, ic, c)
|
||||
|
||||
// wait for done signal and close both connections
|
||||
Logger.Info("Completed websocket hijacking")
|
||||
|
||||
// waiting until d1 or d2 close then automatically defer close both connections
|
||||
select {
|
||||
case <-d1:
|
||||
case <-d2:
|
||||
}
|
||||
}
|
||||
|
||||
// filterWebsocketHeaders allows specific headers to forward to the underlying websocket connection
|
||||
func filterWebsocketHeaders(headers http.Header) (out http.Header) {
|
||||
out = make(http.Header)
|
||||
for k, v := range headers {
|
||||
if k == "Origin" {
|
||||
out[k] = slices.Clone(v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Server) wsRelay(done chan struct{}, a, b *websocket.Conn) {
|
||||
defer func() {
|
||||
close(done)
|
||||
}()
|
||||
for {
|
||||
mt, message, err := a.ReadMessage()
|
||||
if err != nil {
|
||||
Logger.Info("Read message", "err", err)
|
||||
return
|
||||
}
|
||||
if b.WriteMessage(mt, message) != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Remove(c *websocket.Conn) {
|
||||
s.connLock.Lock()
|
||||
delete(s.conns, c.RemoteAddr().String())
|
||||
s.connLock.Unlock()
|
||||
_ = c.Close()
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() {
|
||||
s.connLock.Lock()
|
||||
defer s.connLock.Unlock()
|
||||
|
||||
// flag shutdown and close all open connections
|
||||
s.connStop = true
|
||||
for _, i := range s.conns {
|
||||
_ = i.Close()
|
||||
}
|
||||
|
||||
// clear connections, not required but do it anyway
|
||||
s.conns = make(map[string]*websocket.Conn)
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS redirects
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT,
|
||||
pre INTEGER,
|
||||
destination TEXT,
|
||||
abs INTEGER,
|
||||
code INTEGER,
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
@ -1,14 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS routes
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT,
|
||||
pre INTEGER,
|
||||
destination TEXT,
|
||||
abs INTEGER,
|
||||
cors INTEGER,
|
||||
secure_mode INTEGER,
|
||||
forward_host INTEGER,
|
||||
forward_addr INTEGER,
|
||||
ignore_cert INTEGER,
|
||||
active INTEGER DEFAULT 1
|
||||
);
|
@ -1,228 +1,228 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/target"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"log"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/mrmelon54/rescheduler"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Manager")
|
||||
|
||||
// Manager is a database and mutex wrap around router allowing it to be
|
||||
// dynamically regenerated after updating the database of routes.
|
||||
type Manager struct {
|
||||
db *sql.DB
|
||||
db *database.Queries
|
||||
s *sync.RWMutex
|
||||
r *Router
|
||||
p *httputil.ReverseProxy
|
||||
p *proxy.HybridTransport
|
||||
z *rescheduler.Rescheduler
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed create-table-routes.sql
|
||||
createTableRoutes string
|
||||
//go:embed create-table-redirects.sql
|
||||
createTableRedirects string
|
||||
//go:embed query-table-routes.sql
|
||||
queryTableRoutes string
|
||||
//go:embed query-table-redirects.sql
|
||||
queryTableRedirects string
|
||||
)
|
||||
|
||||
// NewManager create a new manager, initialises the routes and redirects tables
|
||||
// in the database and runs a first time compile.
|
||||
func NewManager(db *sql.DB, proxy *httputil.ReverseProxy) *Manager {
|
||||
func NewManager(db *database.Queries, proxy *proxy.HybridTransport) *Manager {
|
||||
m := &Manager{
|
||||
db: db,
|
||||
s: &sync.RWMutex{},
|
||||
r: New(nil),
|
||||
r: New(proxy),
|
||||
p: proxy,
|
||||
}
|
||||
|
||||
// init routes table
|
||||
_, err := m.db.Exec(createTableRoutes)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate 'routes' table\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// init redirects table
|
||||
_, err = m.db.Exec(createTableRedirects)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Failed to generate 'redirects' table\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// run compile to get the initial router
|
||||
m.Compile()
|
||||
m.z = rescheduler.NewRescheduler(m.threadCompile)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
m.s.RLock()
|
||||
m.r.ServeHTTP(rw, req)
|
||||
r := m.r
|
||||
m.s.RUnlock()
|
||||
r.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
func (m *Manager) Compile() {
|
||||
go func() {
|
||||
// new router
|
||||
router := New(m.p)
|
||||
m.z.Run()
|
||||
}
|
||||
|
||||
// compile router and check errors
|
||||
err := m.internalCompile(router)
|
||||
if err != nil {
|
||||
log.Printf("[Manager] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
func (m *Manager) threadCompile() {
|
||||
// new router
|
||||
router := New(m.p)
|
||||
|
||||
// lock while replacing router
|
||||
m.s.Lock()
|
||||
m.r = router
|
||||
m.s.Unlock()
|
||||
}()
|
||||
// compile router and check errors
|
||||
err := m.internalCompile(router)
|
||||
if err != nil {
|
||||
Logger.Info("Compile failed", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// lock while replacing router
|
||||
m.s.Lock()
|
||||
m.r = router
|
||||
m.s.Unlock()
|
||||
}
|
||||
|
||||
// internalCompile is a hidden internal method for querying the database during
|
||||
// the Compile() method.
|
||||
func (m *Manager) internalCompile(router *Router) error {
|
||||
log.Println("[Manager] Updating routes from database")
|
||||
Logger.Info("Updating routes from database")
|
||||
|
||||
// sql or something?
|
||||
rows, err := m.db.Query(queryTableRoutes)
|
||||
routeRows, err := m.db.GetActiveRoutes(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// loop through rows and scan the options
|
||||
for rows.Next() {
|
||||
var (
|
||||
pre, abs, cors, secure_mode, forward_host, forward_addr, ignore_cert bool
|
||||
src, dst string
|
||||
)
|
||||
err := rows.Scan(&src, &pre, &dst, &abs, &cors, &secure_mode, &forward_host, &forward_addr, &ignore_cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = addRoute(router, src, dst, target.Route{
|
||||
Pre: pre,
|
||||
Abs: abs,
|
||||
Cors: cors,
|
||||
SecureMode: secure_mode,
|
||||
ForwardHost: forward_host,
|
||||
ForwardAddr: forward_addr,
|
||||
IgnoreCert: ignore_cert,
|
||||
for _, row := range routeRows {
|
||||
router.AddRoute(target.Route{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Flags: row.Flags.NormaliseRouteFlags(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// check for errors
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sql or something?
|
||||
rows, err = m.db.Query(queryTableRedirects)
|
||||
redirectsRows, err := m.db.GetActiveRedirects(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// loop through rows and scan the options
|
||||
for rows.Next() {
|
||||
var (
|
||||
pre, abs bool
|
||||
code int
|
||||
src, dst string
|
||||
)
|
||||
err := rows.Scan(&src, &pre, &dst, &abs, &code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = addRedirect(router, src, dst, target.Redirect{
|
||||
Pre: pre,
|
||||
Abs: abs,
|
||||
Code: code,
|
||||
for _, row := range redirectsRows {
|
||||
router.AddRedirect(target.Redirect{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Flags: row.Flags.NormaliseRedirectFlags(),
|
||||
Code: row.Code,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// check for errors
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
// addRoute is an alias to parse the src and dst then add the route
|
||||
func addRoute(router *Router, src string, dst string, t target.Route) error {
|
||||
srcHost, srcPath, dstHost, dstPort, dstPath, err := parseSrcDstHost(src, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update target route values and add route
|
||||
t.Host = dstHost
|
||||
t.Port = dstPort
|
||||
t.Path = dstPath
|
||||
router.AddRoute(srcHost, srcPath, t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addRedirect is an alias to parse the src and dst then add the redirect
|
||||
func addRedirect(router *Router, src string, dst string, t target.Redirect) error {
|
||||
srcHost, srcPath, dstHost, dstPort, dstPath, err := parseSrcDstHost(src, dst)
|
||||
func (m *Manager) GetAllRoutes(hosts []string) ([]target.RouteWithActive, error) {
|
||||
if len(hosts) < 1 {
|
||||
return []target.RouteWithActive{}, nil
|
||||
}
|
||||
|
||||
s := make([]target.RouteWithActive, 0)
|
||||
|
||||
rows, err := m.db.GetAllRoutes(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.Host = dstHost
|
||||
t.Port = dstPort
|
||||
t.Path = dstPath
|
||||
router.AddRedirect(srcHost, srcPath, t)
|
||||
return nil
|
||||
for _, row := range rows {
|
||||
a := target.RouteWithActive{
|
||||
Route: target.Route{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Desc: row.Description,
|
||||
Flags: row.Flags,
|
||||
},
|
||||
Active: row.Active,
|
||||
}
|
||||
|
||||
for _, i := range hosts {
|
||||
// if this is never true then the domain was mistakenly grabbed from the database
|
||||
if a.OnDomain(i) {
|
||||
s = append(s, a)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// parseSrcDstHost extracts the host/path and host:port/path from the src and dst values
|
||||
func parseSrcDstHost(src string, dst string) (string, string, string, int, string, error) {
|
||||
// check if source has path
|
||||
var srcHost, srcPath string
|
||||
nSrc := strings.IndexByte(src, '/')
|
||||
if nSrc == -1 {
|
||||
// set host then path to /
|
||||
srcHost = src
|
||||
srcPath = "/"
|
||||
} else {
|
||||
// set host then custom path
|
||||
srcHost = src[:nSrc]
|
||||
srcPath = src[nSrc:]
|
||||
}
|
||||
|
||||
// check if destination has path
|
||||
var dstPath string
|
||||
nDst := strings.IndexByte(dst, '/')
|
||||
if nDst == -1 {
|
||||
// set path to /
|
||||
dstPath = "/"
|
||||
} else {
|
||||
// set custom path then trim dst string to the host
|
||||
dstPath = dst[nDst:]
|
||||
dst = dst[:nDst]
|
||||
}
|
||||
|
||||
// try to split the destination host into domain + port
|
||||
dstHost, dstPort, ok := utils.SplitDomainPort(dst, 0)
|
||||
if !ok {
|
||||
return "", "", "", 0, "", fmt.Errorf("failed to split destination '%s' into host + port", dst)
|
||||
}
|
||||
|
||||
return srcHost, srcPath, dstHost, dstPort, dstPath, nil
|
||||
func (m *Manager) InsertRoute(route target.RouteWithActive) error {
|
||||
return m.db.AddRoute(context.Background(), database.AddRouteParams{
|
||||
Source: route.Src,
|
||||
Destination: route.Dst,
|
||||
Description: route.Desc,
|
||||
Flags: route.Flags,
|
||||
Active: route.Active,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteRoute(source string) error {
|
||||
return m.db.RemoveRoute(context.Background(), source)
|
||||
}
|
||||
|
||||
func (m *Manager) GetAllRedirects(hosts []string) ([]target.RedirectWithActive, error) {
|
||||
if len(hosts) < 1 {
|
||||
return []target.RedirectWithActive{}, nil
|
||||
}
|
||||
|
||||
s := make([]target.RedirectWithActive, 0)
|
||||
|
||||
rows, err := m.db.GetAllRedirects(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
a := target.RedirectWithActive{
|
||||
Redirect: target.Redirect{
|
||||
Src: row.Source,
|
||||
Dst: row.Destination,
|
||||
Desc: row.Description,
|
||||
Flags: row.Flags,
|
||||
Code: row.Code,
|
||||
},
|
||||
Active: row.Active,
|
||||
}
|
||||
|
||||
for _, i := range hosts {
|
||||
// if this is never true then the domain was mistakenly grabbed from the database
|
||||
if a.OnDomain(i) {
|
||||
s = append(s, a)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *Manager) InsertRedirect(redirect target.RedirectWithActive) error {
|
||||
return m.db.AddRedirect(context.Background(), database.AddRedirectParams{
|
||||
Source: redirect.Src,
|
||||
Destination: redirect.Dst,
|
||||
Description: redirect.Desc,
|
||||
Flags: redirect.Flags,
|
||||
Code: redirect.Code,
|
||||
Active: redirect.Active,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteRedirect(source string) error {
|
||||
return m.db.RemoveRedirect(context.Background(), source)
|
||||
}
|
||||
|
||||
// GenerateHostSearch this should help improve performance
|
||||
// TODO(Melon) discover how to implement this correctly
|
||||
func GenerateHostSearch(hosts []string) (string, []string) {
|
||||
var searchString strings.Builder
|
||||
searchString.WriteString("WHERE ")
|
||||
|
||||
hostArgs := make([]string, len(hosts)*2)
|
||||
for i := range hosts {
|
||||
if i != 0 {
|
||||
searchString.WriteString(" OR ")
|
||||
}
|
||||
// these like checks are not perfect but do reduce load on the database
|
||||
searchString.WriteString("source LIKE '%' + ? + '/%'")
|
||||
searchString.WriteString(" OR source LIKE '%' + ?")
|
||||
|
||||
// loads the hostname into even and odd args
|
||||
hostArgs[i*2] = hosts[i]
|
||||
hostArgs[i*2+1] = hosts[i]
|
||||
}
|
||||
|
||||
return searchString.String(), hostArgs
|
||||
}
|
||||
|
128
router/manager_test.go
Normal file
128
router/manager_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/database"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/target"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeTransport struct{ req *http.Request }
|
||||
|
||||
func (f *fakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
f.req = req
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusOK)
|
||||
return rec.Result(), nil
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
db, err := violet.InitDB("file:TestNewManager?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
ft := &fakeTransport{}
|
||||
ht := proxy.NewHybridTransportWithCalls(ft, ft, &websocket.Server{})
|
||||
m := NewManager(db, ht)
|
||||
assert.NoError(t, m.internalCompile(m.r))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req, err := http.NewRequest(http.MethodGet, "https://test.example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
m.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Nil(t, ft.req)
|
||||
|
||||
err = db.AddRoute(context.Background(), database.AddRouteParams{
|
||||
Source: "*.example.com",
|
||||
Destination: "127.0.0.1:8080",
|
||||
Description: "",
|
||||
Flags: target.FlagAbs | target.FlagForwardHost | target.FlagForwardAddr,
|
||||
Active: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, m.internalCompile(m.r))
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
m.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
assert.NotNil(t, ft.req)
|
||||
}
|
||||
|
||||
func TestManager_GetAllRoutes(t *testing.T) {
|
||||
db, err := violet.InitDB("file:TestManager_GetAllRoutes?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
m := NewManager(db, nil)
|
||||
a := []error{
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.com"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "test.example.com"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.com/hello"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "test.example.com/hello"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.org"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "test.example.org"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "example.org/hello"}, Active: true}),
|
||||
m.InsertRoute(target.RouteWithActive{Route: target.Route{Src: "test.example.org/hello"}, Active: true}),
|
||||
}
|
||||
for _, i := range a {
|
||||
if i != nil {
|
||||
t.Fatal(i)
|
||||
}
|
||||
}
|
||||
routes, err := m.GetAllRoutes([]string{"example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, []target.RouteWithActive{
|
||||
{Route: target.Route{Src: "example.com"}, Active: true},
|
||||
{Route: target.Route{Src: "test.example.com"}, Active: true},
|
||||
{Route: target.Route{Src: "example.com/hello"}, Active: true},
|
||||
{Route: target.Route{Src: "test.example.com/hello"}, Active: true},
|
||||
}, routes)
|
||||
}
|
||||
|
||||
func TestManager_GetAllRedirects(t *testing.T) {
|
||||
db, err := violet.InitDB("file:TestManager_GetAllRedirects?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
m := NewManager(db, nil)
|
||||
a := []error{
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.com"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "test.example.com"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.com/hello"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "test.example.com/hello"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.org"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "test.example.org"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "example.org/hello"}, Active: true}),
|
||||
m.InsertRedirect(target.RedirectWithActive{Redirect: target.Redirect{Src: "test.example.org/hello"}, Active: true}),
|
||||
}
|
||||
for _, i := range a {
|
||||
if i != nil {
|
||||
t.Fatal(i)
|
||||
}
|
||||
}
|
||||
redirects, err := m.GetAllRedirects([]string{"example.com"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, []target.RedirectWithActive{
|
||||
{Redirect: target.Redirect{Src: "example.com"}, Active: true},
|
||||
{Redirect: target.Redirect{Src: "test.example.com"}, Active: true},
|
||||
{Redirect: target.Redirect{Src: "example.com/hello"}, Active: true},
|
||||
{Redirect: target.Redirect{Src: "test.example.com/hello"}, Active: true},
|
||||
}, redirects)
|
||||
}
|
||||
|
||||
func TestGenerateHostSearch(t *testing.T) {
|
||||
query, args := GenerateHostSearch([]string{"example.com", "example.org"})
|
||||
assert.Equal(t, "WHERE source LIKE '%' + ? + '/%' OR source LIKE '%' + ? OR source LIKE '%' + ? + '/%' OR source LIKE '%' + ?", query)
|
||||
assert.Equal(t, []string{"example.com", "example.com", "example.org", "example.org"}, args)
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
select source,
|
||||
pre,
|
||||
destination,
|
||||
abs,
|
||||
code
|
||||
from redirects
|
||||
where active = true
|
@ -1,11 +0,0 @@
|
||||
select source,
|
||||
pre,
|
||||
destination,
|
||||
abs,
|
||||
cors,
|
||||
secure_mode,
|
||||
forward_host,
|
||||
forward_addr,
|
||||
ignore_cert
|
||||
from routes
|
||||
where active = true
|
@ -2,11 +2,11 @@ package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/trie"
|
||||
"github.com/MrMelon54/violet/target"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/mrmelon54/trie"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -14,10 +14,10 @@ type Router struct {
|
||||
route map[string]*trie.Trie[target.Route]
|
||||
redirect map[string]*trie.Trie[target.Redirect]
|
||||
notFound http.Handler
|
||||
proxy *httputil.ReverseProxy
|
||||
proxy *proxy.HybridTransport
|
||||
}
|
||||
|
||||
func New(proxy *httputil.ReverseProxy) *Router {
|
||||
func New(proxy *proxy.HybridTransport) *Router {
|
||||
return &Router{
|
||||
route: make(map[string]*trie.Trie[target.Route]),
|
||||
redirect: make(map[string]*trie.Trie[target.Redirect]),
|
||||
@ -46,16 +46,14 @@ func (r *Router) hostRedirect(host string) *trie.Trie[target.Redirect] {
|
||||
return h
|
||||
}
|
||||
|
||||
func (r *Router) AddService(host string, t target.Route) {
|
||||
r.AddRoute(host, "/", t)
|
||||
}
|
||||
|
||||
func (r *Router) AddRoute(host string, path string, t target.Route) {
|
||||
func (r *Router) AddRoute(t target.Route) {
|
||||
t.Proxy = r.proxy
|
||||
host, path := utils.SplitHostPath(t.Src)
|
||||
r.hostRoute(host).PutString(path, t)
|
||||
}
|
||||
|
||||
func (r *Router) AddRedirect(host, path string, t target.Redirect) {
|
||||
func (r *Router) AddRedirect(t target.Redirect) {
|
||||
host, path := utils.SplitHostPath(t.Src)
|
||||
r.hostRedirect(host).PutString(path, t)
|
||||
}
|
||||
|
||||
@ -86,33 +84,35 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if r.serveRouteHTTP(rw, req, wildcardHost) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondVioletError(rw, http.StatusTeapot, "No route")
|
||||
}
|
||||
|
||||
func (r *Router) serveRouteHTTP(rw http.ResponseWriter, req *http.Request, host string) bool {
|
||||
h := r.route[host]
|
||||
if h != nil {
|
||||
pairs := h.GetAllKeyValues([]byte(req.URL.Path))
|
||||
for i := len(pairs) - 1; i >= 0; i-- {
|
||||
if pairs[i].Value.Pre || pairs[i].Key == req.URL.Path {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, pairs[i].Key)
|
||||
pairs[i].Value.ServeHTTP(rw, req)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return getServeData(rw, req, h)
|
||||
}
|
||||
|
||||
func (r *Router) serveRedirectHTTP(rw http.ResponseWriter, req *http.Request, host string) bool {
|
||||
h := r.redirect[host]
|
||||
if h != nil {
|
||||
pairs := h.GetAllKeyValues([]byte(req.URL.Path))
|
||||
for i := len(pairs) - 1; i >= 0; i-- {
|
||||
if pairs[i].Value.Pre || pairs[i].Key == req.URL.Path {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, pairs[i].Key)
|
||||
pairs[i].Value.ServeHTTP(rw, req)
|
||||
return true
|
||||
}
|
||||
return getServeData(rw, req, h)
|
||||
}
|
||||
|
||||
type serveDataInterface interface {
|
||||
HasFlag(flag target.Flags) bool
|
||||
ServeHTTP(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
func getServeData[T serveDataInterface](rw http.ResponseWriter, req *http.Request, h *trie.Trie[T]) bool {
|
||||
if h == nil {
|
||||
return false
|
||||
}
|
||||
pairs := h.GetAllKeyValues([]byte(req.URL.Path))
|
||||
for i := len(pairs) - 1; i >= 0; i-- {
|
||||
if pairs[i].Value.HasFlag(target.FlagPre) || pairs[i].Key == req.URL.Path {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, pairs[i].Key)
|
||||
pairs[i].Value.ServeHTTP(rw, req)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -1,13 +1,25 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/MrMelon54/violet/target"
|
||||
"fmt"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/mrmelon54/trie"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type routeTestBase struct {
|
||||
path string
|
||||
dst target.Route
|
||||
tests map[string]string
|
||||
}
|
||||
|
||||
type redirectTestBase struct {
|
||||
path string
|
||||
dst target.Redirect
|
||||
@ -16,79 +28,214 @@ type redirectTestBase struct {
|
||||
|
||||
type mss map[string]string
|
||||
|
||||
var redirectTests = []redirectTestBase{
|
||||
{"/", target.Redirect{}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Path: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Abs: true}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Abs: true, Path: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Pre: true}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/hello",
|
||||
}},
|
||||
{"/", target.Redirect{Pre: true, Path: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world/hello",
|
||||
}},
|
||||
{"/", target.Redirect{Pre: true, Abs: true}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/",
|
||||
}},
|
||||
{"/", target.Redirect{Pre: true, Abs: true, Path: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world",
|
||||
}},
|
||||
{"/hello", target.Redirect{}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Path: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Abs: true}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Abs: true, Path: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Pre: true}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/hi",
|
||||
}},
|
||||
{"/hello", target.Redirect{Pre: true, Path: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world/hi",
|
||||
}},
|
||||
{"/hello", target.Redirect{Pre: true, Abs: true}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/",
|
||||
}},
|
||||
{"/hello", target.Redirect{Pre: true, Abs: true, Path: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world",
|
||||
}},
|
||||
var (
|
||||
routeTests = []routeTestBase{
|
||||
{"/", target.Route{}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Route{Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagAbs}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagPre}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/hello",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagPre, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world/hello",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagPre | target.FlagAbs}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/",
|
||||
}},
|
||||
{"/", target.Route{Flags: target.FlagPre | target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world",
|
||||
}},
|
||||
{"/hello", target.Route{}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Route{Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagAbs}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagPre}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/hi",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagPre, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world/hi",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagPre | target.FlagAbs}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/",
|
||||
}},
|
||||
{"/hello", target.Route{Flags: target.FlagPre | target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world",
|
||||
}},
|
||||
}
|
||||
redirectTests = []redirectTestBase{
|
||||
{"/", target.Redirect{}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagAbs}, mss{
|
||||
"/": "/",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagPre}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/hello",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagPre, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world/hello",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagPre | target.FlagAbs}, mss{
|
||||
"/": "/",
|
||||
"/hello": "/",
|
||||
}},
|
||||
{"/", target.Redirect{Flags: target.FlagPre | target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "/world",
|
||||
"/hello": "/world",
|
||||
}},
|
||||
{"/hello", target.Redirect{}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagAbs}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagPre}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/hi",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagPre, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world/hi",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagPre | target.FlagAbs}, mss{
|
||||
"/": "",
|
||||
"/hello": "/",
|
||||
"/hello/hi": "/",
|
||||
}},
|
||||
{"/hello", target.Redirect{Flags: target.FlagPre | target.FlagAbs, Dst: "world"}, mss{
|
||||
"/": "",
|
||||
"/hello": "/world",
|
||||
"/hello/hi": "/world",
|
||||
}},
|
||||
}
|
||||
)
|
||||
|
||||
func TestRouter_AddRoute(t *testing.T) {
|
||||
transSecure := &fakeTransport{}
|
||||
transInsecure := &fakeTransport{}
|
||||
|
||||
for _, i := range routeTests {
|
||||
r := New(proxy.NewHybridTransportWithCalls(transSecure, transInsecure, &websocket.Server{}))
|
||||
dst := i.dst
|
||||
dst.Dst = path.Join("127.0.0.1:8080", dst.Dst)
|
||||
dst.Src = path.Join("example.com", i.path)
|
||||
t.Logf("Running tests for %#v\n", dst)
|
||||
r.AddRoute(dst)
|
||||
for k, v := range i.tests {
|
||||
u1 := &url.URL{Scheme: "https", Host: "example.com", Path: k}
|
||||
req, _ := http.NewRequest(http.MethodGet, u1.String(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if v == "" {
|
||||
if transSecure.req != nil {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["example.com"].String())
|
||||
t.Fatalf("%s => %s\n", k, v)
|
||||
}
|
||||
} else {
|
||||
if transSecure.req == nil {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["example.com"].String())
|
||||
t.Fatalf("\nexpected %s => %s\n got %s => %s\n", k, v, k, "")
|
||||
}
|
||||
if v != transSecure.req.URL.Path {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["example.com"].String())
|
||||
t.Fatalf("\nexpected %s => %s\n got %s => %s\n", k, v, k, transSecure.req.URL.Path)
|
||||
}
|
||||
transSecure.req = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_AddRedirect(t *testing.T) {
|
||||
for _, i := range redirectTests {
|
||||
r := New(nil)
|
||||
dst := i.dst
|
||||
dst.Dst = path.Join("example.com", dst.Dst)
|
||||
dst.Code = http.StatusFound
|
||||
dst.Src = path.Join("www.example.com", i.path)
|
||||
t.Logf("Running tests for %#v\n", dst)
|
||||
r.AddRedirect(dst)
|
||||
for k, v := range i.tests {
|
||||
u1 := &url.URL{Scheme: "https", Host: "example.com", Path: v}
|
||||
if v == "" {
|
||||
u1 = nil
|
||||
}
|
||||
u2 := &url.URL{Scheme: "https", Host: "www.example.com", Path: k}
|
||||
assertHttpRedirect(t, r, http.StatusFound, outputUrl(u1), http.MethodGet, outputUrl(u2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertHttpRedirect(t *testing.T, r *Router, code int, target, method, start string) {
|
||||
@ -111,28 +258,69 @@ func assertHttpRedirect(t *testing.T, r *Router, code int, target, method, start
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouter_AddRedirect(t *testing.T) {
|
||||
for _, i := range redirectTests {
|
||||
r := New(nil)
|
||||
dst := i.dst
|
||||
dst.Host = "example.com"
|
||||
dst.Code = http.StatusFound
|
||||
t.Logf("Running tests for %#v\n", dst)
|
||||
r.AddRedirect("www.example.com", i.path, dst)
|
||||
for k, v := range i.tests {
|
||||
u1 := &url.URL{Scheme: "https", Host: "example.com", Path: v}
|
||||
if v == "" {
|
||||
u1 = nil
|
||||
}
|
||||
u2 := &url.URL{Scheme: "https", Host: "www.example.com", Path: k}
|
||||
assertHttpRedirect(t, r, http.StatusFound, outputUrl(u1), http.MethodGet, outputUrl(u2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func outputUrl(u *url.URL) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func TestRouter_AddWildcardRoute(t *testing.T) {
|
||||
transSecure := &fakeTransport{}
|
||||
transInsecure := &fakeTransport{}
|
||||
|
||||
for _, i := range routeTests {
|
||||
r := New(proxy.NewHybridTransportWithCalls(transSecure, transInsecure, &websocket.Server{}))
|
||||
dst := i.dst
|
||||
dst.Dst = path.Join("127.0.0.1:8080", dst.Dst)
|
||||
dst.Src = path.Join("*.example.com", i.path)
|
||||
t.Logf("Running tests for %#v\n", dst)
|
||||
r.AddRoute(dst)
|
||||
for k, v := range i.tests {
|
||||
u1 := &url.URL{Scheme: "https", Host: "test.example.com", Path: k}
|
||||
req, _ := http.NewRequest(http.MethodGet, u1.String(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
if v == "" {
|
||||
if transSecure.req != nil {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["*.example.com"].String())
|
||||
t.Fatalf("%s => %s\n", k, v)
|
||||
}
|
||||
} else {
|
||||
if transSecure.req == nil {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["*.example.com"].String())
|
||||
t.Fatalf("\nexpected %s => %s\n got %s => %s\n", k, v, k, "")
|
||||
}
|
||||
if v != transSecure.req.URL.Path {
|
||||
t.Logf("Test URL: %#v\n", req.URL)
|
||||
t.Log(r.route["*.example.com"].String())
|
||||
t.Fatalf("\nexpected %s => %s\n got %s => %s\n", k, v, k, transSecure.req.URL.Path)
|
||||
}
|
||||
transSecure.req = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRoundTripper struct{}
|
||||
|
||||
func (f *fakeRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusNotFound)
|
||||
return rec.Result(), nil
|
||||
}
|
||||
|
||||
func TestGetServeData_Route(t *testing.T) {
|
||||
hyb := proxy.NewHybridTransportWithCalls(&fakeRoundTripper{}, &fakeRoundTripper{}, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/hello/world/this/is/a/test", nil)
|
||||
assert.NoError(t, err)
|
||||
h := trie.BuildFromMap(map[string]target.Route{
|
||||
"/hello/world": {Flags: target.FlagPre, Proxy: hyb},
|
||||
})
|
||||
rec := httptest.NewRecorder()
|
||||
pairs := h.GetAllKeyValues([]byte(req.URL.Path))
|
||||
fmt.Printf("%#v\n", pairs)
|
||||
assert.True(t, getServeData(rec, req, h))
|
||||
}
|
||||
|
@ -1,60 +0,0 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer-utils/claims/auth"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/mrmelon54/mjwt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewApiServer creates and runs a http server containing all the API
|
||||
// endpoints for the software
|
||||
//
|
||||
// `/compile` - reloads all domains, routes and redirects
|
||||
func NewApiServer(conf *Conf, compileTarget utils.MultiCompilable) *http.Server {
|
||||
r := httprouter.New()
|
||||
|
||||
// Endpoint for compile action
|
||||
r.POST("/compile", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
|
||||
// Get bearer token
|
||||
bearer := utils.GetBearer(req)
|
||||
if bearer == "" {
|
||||
utils.RespondHttpStatus(rw, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Read claims from mjwt
|
||||
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](conf.Verify, bearer)
|
||||
if err != nil {
|
||||
utils.RespondHttpStatus(rw, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Token must have `violet:compile` perm
|
||||
if !b.Claims.Perms.Has("violet:compile") {
|
||||
utils.RespondHttpStatus(rw, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger the compile action
|
||||
compileTarget.Compile()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
|
||||
// Create and run http server
|
||||
s := &http.Server{
|
||||
Addr: conf.ApiListen,
|
||||
Handler: r,
|
||||
ReadTimeout: time.Minute,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
WriteTimeout: time.Minute,
|
||||
IdleTimeout: time.Minute,
|
||||
MaxHeaderBytes: 2500,
|
||||
}
|
||||
log.Printf("[API] Starting API server on: '%s'\n", s.Addr)
|
||||
go utils.RunBackgroundHttp("API", s)
|
||||
return s
|
||||
}
|
112
servers/api/api.go
Normal file
112
servers/api/api.go
Normal file
@ -0,0 +1,112 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/claims"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewApiServer creates and runs a http server containing all the API
|
||||
// endpoints for the software
|
||||
//
|
||||
// `/compile` - reloads all domains, routes and redirects
|
||||
func NewApiServer(conf *conf.Conf, compileTarget utils.MultiCompilable, registry *prometheus.Registry) *http.Server {
|
||||
r := httprouter.New()
|
||||
|
||||
r.GET("/", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
http.Error(rw, "Violet API Endpoint", http.StatusOK)
|
||||
})
|
||||
r.GET("/metrics", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(rw, req)
|
||||
})
|
||||
|
||||
// Endpoint for compile action
|
||||
r.POST("/compile", checkAuthWithPerm(conf.Signer, "violet:compile", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params, b AuthClaims) {
|
||||
// Trigger the compile action
|
||||
compileTarget.Compile()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
|
||||
// Endpoint for domains
|
||||
domainFunc := domainManage(conf.Signer, conf.Domains)
|
||||
r.PUT("/domain/:domain", domainFunc)
|
||||
r.DELETE("/domain/:domain", domainFunc)
|
||||
|
||||
SetupTargetApis(r, conf.Signer, conf.Router)
|
||||
|
||||
// Endpoint for acme-challenge
|
||||
acmeChallengeFunc := acmeChallengeManage(conf.Signer, conf.Domains, conf.Acme)
|
||||
r.PUT("/acme-challenge/:domain/:key/:value", acmeChallengeFunc)
|
||||
r.DELETE("/acme-challenge/:domain/:key", acmeChallengeFunc)
|
||||
|
||||
// Create and run http server
|
||||
return &http.Server{
|
||||
Handler: r,
|
||||
ReadTimeout: time.Minute,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
WriteTimeout: time.Minute,
|
||||
IdleTimeout: time.Minute,
|
||||
MaxHeaderBytes: 2500,
|
||||
}
|
||||
}
|
||||
|
||||
// apiError outputs a generic JSON error message
|
||||
func apiError(rw http.ResponseWriter, code int, m string) {
|
||||
rw.WriteHeader(code)
|
||||
_ = json.NewEncoder(rw).Encode(map[string]string{
|
||||
"error": m,
|
||||
})
|
||||
}
|
||||
|
||||
func domainManage(verify mjwt.Verifier, domains utils.DomainProvider) httprouter.Handle {
|
||||
return checkAuthWithPerm(verify, "violet:domains", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
// add domain with active state
|
||||
domains.Put(params.ByName("domain"), req.Method == http.MethodPut)
|
||||
domains.Compile()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
}
|
||||
|
||||
func acmeChallengeManage(verify mjwt.Verifier, domains utils.DomainProvider, acme utils.AcmeChallengeProvider) httprouter.Handle {
|
||||
return checkAuthWithPerm(verify, "violet:acme-challenge", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
domain := params.ByName("domain")
|
||||
if !domains.IsValid(domain) {
|
||||
utils.RespondVioletError(rw, http.StatusBadRequest, "Invalid ACME challenge domain")
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodPut {
|
||||
acme.Put(domain, params.ByName("key"), params.ByName("value"))
|
||||
} else {
|
||||
acme.Delete(domain, params.ByName("key"))
|
||||
}
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
}
|
||||
|
||||
// getDomainOwnershipClaims returns the domains marked as owned from PermStorage,
|
||||
// they match `domain:owns=<fqdn>` where fqdn will be returned
|
||||
func getDomainOwnershipClaims(perms *claims.PermStorage) []string {
|
||||
a := perms.Search("domain:owns=*")
|
||||
for i := range a {
|
||||
a[i] = a[i][len("domain:owns="):]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// validateDomainOwnershipClaims validates if the claims contain the
|
||||
// `domain:owns=<fqdn>` field with the matching top level domain
|
||||
func validateDomainOwnershipClaims(a string, perms *claims.PermStorage) bool {
|
||||
if fqdn, ok := utils.GetTopFqdn(a); ok {
|
||||
if perms.Has("domain:owns=" + fqdn) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
127
servers/api/api_test.go
Normal file
127
servers/api/api_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/1f349/violet/utils/fake"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewApiServer_Compile(t *testing.T) {
|
||||
apiConf := &conf.Conf{
|
||||
Domains: &fake.Domains{},
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
f := &fake.Compilable{}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{f}, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com/compile", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
assert.False(t, f.Done)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+fake.GenSnakeOilKey("violet:compile"))
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.True(t, f.Done)
|
||||
}
|
||||
|
||||
func TestNewApiServer_AcmeChallenge_Put(t *testing.T) {
|
||||
apiConf := &conf.Conf{
|
||||
Domains: &fake.Domains{},
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil)
|
||||
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
|
||||
|
||||
// Valid domain
|
||||
req, err := http.NewRequest(http.MethodPut, "https://example.com/acme-challenge/example.com/123/123abc", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+acmeKey)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, "123abc", apiConf.Acme.Get("example.com", "123"))
|
||||
|
||||
// Invalid domain
|
||||
req, err = http.NewRequest(http.MethodPut, "https://example.com/acme-challenge/notexample.com/123/123abc", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+acmeKey)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
assert.Equal(t, "Invalid ACME challenge domain", res.Header.Get("X-Violet-Error"))
|
||||
}
|
||||
|
||||
func TestNewApiServer_AcmeChallenge_Delete(t *testing.T) {
|
||||
apiConf := &conf.Conf{
|
||||
Domains: &fake.Domains{},
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewApiServer(apiConf, utils.MultiCompilable{}, nil)
|
||||
acmeKey := fake.GenSnakeOilKey("violet:acme-challenge")
|
||||
|
||||
// Valid domain
|
||||
req, err := http.NewRequest(http.MethodDelete, "https://example.com/acme-challenge/example.com/123", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+acmeKey)
|
||||
apiConf.Acme.Put("example.com", "123", "123abc")
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
assert.Equal(t, "", apiConf.Acme.Get("example.com", "123"))
|
||||
|
||||
// Invalid domain
|
||||
req, err = http.NewRequest(http.MethodDelete, "https://example.com/acme-challenge/notexample.com/123", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusForbidden, res.StatusCode)
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+acmeKey)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
assert.Equal(t, "Invalid ACME challenge domain", res.Header.Get("X-Violet-Error"))
|
||||
}
|
49
servers/api/auth.go
Normal file
49
servers/api/auth.go
Normal file
@ -0,0 +1,49 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/auth"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthClaims mjwt.BaseTypeClaims[auth.AccessTokenClaims]
|
||||
|
||||
type AuthCallback func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims)
|
||||
|
||||
// checkAuth validates the bearer token against a mjwt.Verifier and returns an
|
||||
// error message or continues to the next handler
|
||||
func checkAuth(verify mjwt.Verifier, cb AuthCallback) httprouter.Handle {
|
||||
return func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
// Get bearer token
|
||||
bearer := utils.GetBearer(req)
|
||||
if bearer == "" {
|
||||
apiError(rw, http.StatusForbidden, "Missing bearer token")
|
||||
return
|
||||
}
|
||||
|
||||
// Read claims from mjwt
|
||||
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](verify, bearer)
|
||||
if err != nil {
|
||||
apiError(rw, http.StatusForbidden, "Invalid token")
|
||||
return
|
||||
}
|
||||
|
||||
cb(rw, req, params, AuthClaims(b))
|
||||
}
|
||||
}
|
||||
|
||||
// checkAuthWithPerm validates the bearer token and checks if it contains a
|
||||
// required permission and returns an error message or continues to the next
|
||||
// handler
|
||||
func checkAuthWithPerm(verify mjwt.Verifier, perm string, cb AuthCallback) httprouter.Handle {
|
||||
return checkAuth(verify, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
// check perms
|
||||
if !b.Claims.Perms.Has(perm) {
|
||||
apiError(rw, http.StatusForbidden, "No permission")
|
||||
return
|
||||
}
|
||||
cb(rw, req, params, b)
|
||||
})
|
||||
}
|
27
servers/api/target-types.go
Normal file
27
servers/api/target-types.go
Normal file
@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/1f349/violet/target"
|
||||
)
|
||||
|
||||
type sourceJson struct {
|
||||
Src string `json:"src"`
|
||||
}
|
||||
|
||||
func (s sourceJson) GetSource() string { return s.Src }
|
||||
|
||||
type routeSource target.RouteWithActive
|
||||
|
||||
func (r routeSource) GetSource() string { return r.Src }
|
||||
|
||||
type redirectSource target.RedirectWithActive
|
||||
|
||||
func (r redirectSource) GetSource() string { return r.Src }
|
||||
|
||||
var (
|
||||
_ sourceGetter = sourceJson{}
|
||||
_ sourceGetter = routeSource{}
|
||||
_ sourceGetter = redirectSource{}
|
||||
)
|
||||
|
||||
type sourceGetter interface{ GetSource() string }
|
105
servers/api/target.go
Normal file
105
servers/api/target.go
Normal file
@ -0,0 +1,105 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/target"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SetupTargetApis(r *httprouter.Router, verify mjwt.Verifier, manager *router.Manager) {
|
||||
// Endpoint for routes
|
||||
r.GET("/route", checkAuthWithPerm(verify, "violet:route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
domains := getDomainOwnershipClaims(b.Claims.Perms)
|
||||
|
||||
routes, err := manager.GetAllRoutes(domains)
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to get routes from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to get routes from database")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(rw).Encode(routes)
|
||||
}))
|
||||
r.POST("/route", parseJsonAndCheckOwnership[routeSource](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t routeSource) {
|
||||
err := manager.InsertRoute(target.RouteWithActive(t))
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to insert route into database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to insert route into database")
|
||||
return
|
||||
}
|
||||
manager.Compile()
|
||||
}))
|
||||
r.DELETE("/route", parseJsonAndCheckOwnership[sourceJson](verify, "route", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) {
|
||||
err := manager.DeleteRoute(t.Src)
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to delete route from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete route from database")
|
||||
return
|
||||
}
|
||||
manager.Compile()
|
||||
}))
|
||||
|
||||
// Endpoint for redirects
|
||||
r.GET("/redirect", checkAuthWithPerm(verify, "violet:redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
domains := getDomainOwnershipClaims(b.Claims.Perms)
|
||||
|
||||
redirects, err := manager.GetAllRedirects(domains)
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to get redirects from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to get redirects from database")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(rw).Encode(redirects)
|
||||
}))
|
||||
r.POST("/redirect", parseJsonAndCheckOwnership[redirectSource](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t redirectSource) {
|
||||
err := manager.InsertRedirect(target.RedirectWithActive(t))
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to insert redirect into database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to insert redirect into database")
|
||||
return
|
||||
}
|
||||
manager.Compile()
|
||||
}))
|
||||
r.DELETE("/redirect", parseJsonAndCheckOwnership[sourceJson](verify, "redirect", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t sourceJson) {
|
||||
err := manager.DeleteRedirect(t.Src)
|
||||
if err != nil {
|
||||
logger.Logger.Infof("Failed to delete redirect from database: %s\n", err)
|
||||
apiError(rw, http.StatusInternalServerError, "Failed to delete redirect from database")
|
||||
return
|
||||
}
|
||||
manager.Compile()
|
||||
}))
|
||||
}
|
||||
|
||||
type AuthWithJsonCallback[T any] func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims, t T)
|
||||
|
||||
func parseJsonAndCheckOwnership[T sourceGetter](verify mjwt.Verifier, t string, cb AuthWithJsonCallback[T]) httprouter.Handle {
|
||||
return checkAuthWithPerm(verify, "violet:"+t, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, b AuthClaims) {
|
||||
var j T
|
||||
if json.NewDecoder(req.Body).Decode(&j) != nil {
|
||||
apiError(rw, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// check token owns this domain
|
||||
host, _ := utils.SplitHostPath(j.GetSource())
|
||||
if strings.IndexByte(host, ':') != -1 {
|
||||
apiError(rw, http.StatusBadRequest, "Invalid route source")
|
||||
return
|
||||
}
|
||||
|
||||
if !validateDomainOwnershipClaims(host, b.Claims.Perms) {
|
||||
apiError(rw, http.StatusBadRequest, "Token cannot modify the specified domain")
|
||||
return
|
||||
}
|
||||
|
||||
cb(rw, req, params, b, j)
|
||||
})
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/MrMelon54/violet/certs"
|
||||
"github.com/MrMelon54/violet/domains"
|
||||
errorPages "github.com/MrMelon54/violet/error-pages"
|
||||
"github.com/MrMelon54/violet/favicons"
|
||||
"github.com/MrMelon54/violet/router"
|
||||
"github.com/mrmelon54/mjwt"
|
||||
)
|
||||
|
||||
// Conf stores the shared configuration for the API, HTTP and HTTPS servers.
|
||||
type Conf struct {
|
||||
ApiListen string // api server listen address
|
||||
HttpListen string // http server listen address
|
||||
HttpsListen string // https server listen address
|
||||
DB *sql.DB
|
||||
Domains *domains.Domains
|
||||
Certs *certs.Certs
|
||||
Favicons *favicons.Favicons
|
||||
Verify mjwt.Provider
|
||||
ErrorPages *errorPages.ErrorPages
|
||||
Router *router.Manager
|
||||
}
|
23
servers/conf/conf.go
Normal file
23
servers/conf/conf.go
Normal file
@ -0,0 +1,23 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/violet/database"
|
||||
errorPages "github.com/1f349/violet/error-pages"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/utils"
|
||||
)
|
||||
|
||||
// Conf stores the shared configuration for the API, HTTP and HTTPS servers.
|
||||
type Conf struct {
|
||||
RateLimit uint64 // rate limit per minute
|
||||
DB *database.Queries
|
||||
Domains utils.DomainProvider
|
||||
Acme utils.AcmeChallengeProvider
|
||||
Certs utils.CertProvider
|
||||
Favicons *favicons.Favicons
|
||||
Signer mjwt.Verifier
|
||||
ErrorPages *errorPages.ErrorPages
|
||||
Router *router.Manager
|
||||
}
|
@ -2,9 +2,11 @@ package servers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/servers/metrics"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
@ -14,82 +16,63 @@ import (
|
||||
// endpoints for the reverse proxy.
|
||||
//
|
||||
// `/.well-known/acme-challenge/{token}` is used for outputting answers for
|
||||
// acme challenges, this is used for Lets Encrypt HTTP verification.
|
||||
func NewHttpServer(conf *Conf) *http.Server {
|
||||
// acme challenges, this is used for Let's Encrypt HTTP verification.
|
||||
func NewHttpServer(httpsPort uint16, conf *conf.Conf, registry *prometheus.Registry) *http.Server {
|
||||
r := httprouter.New()
|
||||
var secureExtend string
|
||||
_, httpsPort, ok := utils.SplitDomainPort(conf.HttpsListen, 443)
|
||||
if !ok {
|
||||
httpsPort = 443
|
||||
}
|
||||
if httpsPort != 443 {
|
||||
secureExtend = fmt.Sprintf(":%d", httpsPort)
|
||||
}
|
||||
|
||||
// Endpoint for acme challenge outputs
|
||||
r.GET("/.well-known/acme-challenge/{key}", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
if h, ok := utils.GetDomainWithoutPort(req.Host); ok {
|
||||
// check if the host is valid
|
||||
if !conf.Domains.IsValid(req.Host) {
|
||||
http.Error(rw, fmt.Sprintf("%d %s\n", 420, "Invalid host"), 420)
|
||||
return
|
||||
}
|
||||
r.GET("/.well-known/acme-challenge/:key", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
||||
h := utils.GetDomainWithoutPort(req.Host)
|
||||
|
||||
// check if the key is valid
|
||||
key := params.ByName("key")
|
||||
if key == "" {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// prepare for executing query
|
||||
prepare, err := conf.DB.Prepare("select value from acme_challenges limit 1 where domain = ? and key = ?")
|
||||
if err != nil {
|
||||
utils.RespondHttpStatus(rw, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// query the row and extract the value
|
||||
row := prepare.QueryRow(h, key)
|
||||
var value string
|
||||
err = row.Scan(&value)
|
||||
if err != nil {
|
||||
utils.RespondHttpStatus(rw, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// output response
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte(value))
|
||||
// check if the host is valid
|
||||
if !conf.Domains.IsValid(req.Host) {
|
||||
utils.RespondVioletError(rw, http.StatusBadRequest, "Invalid host")
|
||||
return
|
||||
}
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
// check if the key is valid
|
||||
value := conf.Acme.Get(h, params.ByName("key"))
|
||||
if value == "" {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// output response
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte(value))
|
||||
})
|
||||
|
||||
// All other paths lead here and are forwarded to HTTPS
|
||||
r.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if h, ok := utils.GetDomainWithoutPort(req.Host); ok {
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: h + secureExtend,
|
||||
Path: req.URL.Path,
|
||||
RawPath: req.URL.RawPath,
|
||||
RawQuery: req.URL.RawQuery,
|
||||
}
|
||||
utils.FastRedirect(rw, req, u.String(), http.StatusPermanentRedirect)
|
||||
h := utils.GetDomainWithoutPort(req.Host)
|
||||
u := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: h + secureExtend,
|
||||
Path: req.URL.Path,
|
||||
RawPath: req.URL.RawPath,
|
||||
RawQuery: req.URL.RawQuery,
|
||||
}
|
||||
utils.FastRedirect(rw, req, u.String(), http.StatusPermanentRedirect)
|
||||
})
|
||||
|
||||
metricsMiddleware := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
r.ServeHTTP(rw, req)
|
||||
})
|
||||
if registry != nil {
|
||||
metricsMiddleware = metrics.New(registry, nil).WrapHandler("violet-http-insecure", r)
|
||||
}
|
||||
|
||||
// Create and run http server
|
||||
s := &http.Server{
|
||||
Addr: conf.HttpListen,
|
||||
Handler: r,
|
||||
return &http.Server{
|
||||
Handler: metricsMiddleware,
|
||||
ReadTimeout: time.Minute,
|
||||
ReadHeaderTimeout: time.Minute,
|
||||
WriteTimeout: time.Minute,
|
||||
IdleTimeout: time.Minute,
|
||||
MaxHeaderBytes: 2500,
|
||||
}
|
||||
log.Printf("[HTTP] Starting HTTP server on: '%s'\n", s.Addr)
|
||||
go utils.RunBackgroundHttp("HTTP", s)
|
||||
return s
|
||||
}
|
||||
|
48
servers/http_test.go
Normal file
48
servers/http_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/1f349/violet/utils/fake"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewHttpServer_AcmeChallenge(t *testing.T) {
|
||||
httpConf := &conf.Conf{
|
||||
Domains: &fake.Domains{},
|
||||
Acme: utils.NewAcmeChallenge(),
|
||||
Signer: fake.SnakeOilProv,
|
||||
}
|
||||
srv := NewHttpServer(443, httpConf, nil)
|
||||
httpConf.Acme.Put("example.com", "456", "456def")
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/456", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
all, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, bytes.Compare([]byte("456def"), all))
|
||||
|
||||
// Invalid key
|
||||
req, err = http.NewRequest(http.MethodGet, "https://example.com/.well-known/acme-challenge/789", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
rec = httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res = rec.Result()
|
||||
assert.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||
|
||||
all, err = io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, bytes.Compare([]byte(""), all))
|
||||
}
|
122
servers/https.go
122
servers/https.go
@ -3,68 +3,126 @@ package servers
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/1f349/violet/favicons"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/servers/metrics"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sethvargo/go-limiter/httplimit"
|
||||
"github.com/sethvargo/go-limiter/memorystore"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewHttpsServer creates and runs a http server containing the public https
|
||||
// endpoints for the reverse proxy.
|
||||
func NewHttpsServer(conf *Conf) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: conf.HttpsListen,
|
||||
Handler: setupRateLimiter(300).Middleware(conf.Router),
|
||||
DisableGeneralOptionsHandler: false,
|
||||
TLSConfig: &tls.Config{GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
// error out on invalid domains
|
||||
if !conf.Domains.IsValid(info.ServerName) {
|
||||
return nil, fmt.Errorf("invalid hostname used: '%s'", info.ServerName)
|
||||
}
|
||||
func NewHttpsServer(conf *conf.Conf, registry *prometheus.Registry) *http.Server {
|
||||
r := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
logger.Logger.Debug("Request", "method", req.Method, "url", req.URL, "remote", req.RemoteAddr, "host", req.Host, "length", req.ContentLength, "goroutine", runtime.NumGoroutine())
|
||||
conf.Router.ServeHTTP(rw, req)
|
||||
})
|
||||
favMiddleware := setupFaviconMiddleware(conf.Favicons, r)
|
||||
|
||||
// find a certificate
|
||||
cert := conf.Certs.GetCertForDomain(info.ServerName)
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("failed to find certificate for: '%s'", info.ServerName)
|
||||
}
|
||||
metricsMeta := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
r.ServeHTTP(rw, req)
|
||||
})
|
||||
if registry != nil {
|
||||
metricsMiddleware := metrics.New(registry, nil).WrapHandler("violet-https", favMiddleware)
|
||||
metricsMeta = func(rw http.ResponseWriter, req *http.Request) {
|
||||
metricsMiddleware.ServeHTTP(rw, metrics.AddHostCtx(req))
|
||||
}
|
||||
}
|
||||
rateLimiter := setupRateLimiter(conf.RateLimit, metricsMeta)
|
||||
hsts := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
rateLimiter.ServeHTTP(rw, req)
|
||||
})
|
||||
|
||||
// time to return
|
||||
return cert, nil
|
||||
}},
|
||||
return &http.Server{
|
||||
Handler: hsts,
|
||||
TLSConfig: &tls.Config{
|
||||
// Suggested by https://ssl-config.mozilla.org/#server=go&version=1.21.5&config=intermediate
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
// error out on invalid domains
|
||||
if !conf.Domains.IsValid(info.ServerName) {
|
||||
return nil, fmt.Errorf("invalid hostname used: '%s'", info.ServerName)
|
||||
}
|
||||
|
||||
// find a certificate
|
||||
cert := conf.Certs.GetCertForDomain(info.ServerName)
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("failed to find certificate for: '%s'", info.ServerName)
|
||||
}
|
||||
|
||||
// time to return
|
||||
return cert, nil
|
||||
},
|
||||
},
|
||||
ReadTimeout: 150 * time.Second,
|
||||
ReadHeaderTimeout: 150 * time.Second,
|
||||
WriteTimeout: 150 * time.Second,
|
||||
IdleTimeout: 150 * time.Second,
|
||||
MaxHeaderBytes: 4096000,
|
||||
ConnState: func(conn net.Conn, state http.ConnState) {
|
||||
fmt.Printf("%s => %s: %s\n", conn.LocalAddr(), conn.RemoteAddr(), state.String())
|
||||
},
|
||||
}
|
||||
log.Printf("[HTTPS] Starting HTTPS server on: '%s'\n", s.Addr)
|
||||
go utils.RunBackgroundHttps("HTTPS", s)
|
||||
return s
|
||||
}
|
||||
|
||||
// setupRateLimiter is an internal function to create a middleware to manage
|
||||
// rate limits.
|
||||
func setupRateLimiter(rateLimit uint64) mux.MiddlewareFunc {
|
||||
func setupRateLimiter(rateLimit uint64, next http.Handler) http.Handler {
|
||||
// create memory store
|
||||
store, err := memorystore.New(&memorystore.Config{
|
||||
Tokens: rateLimit,
|
||||
Interval: time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
logger.Logger.Fatal("Failed to initialize memory store", "err", err)
|
||||
}
|
||||
|
||||
// create a middleware using ips as the key for rate limits
|
||||
middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
logger.Logger.Fatal("Failed to initialize httplimit middleware", "err", err)
|
||||
}
|
||||
return middleware.Handle
|
||||
return middleware.Handle(next)
|
||||
}
|
||||
|
||||
func setupFaviconMiddleware(fav *favicons.Favicons, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if req.Header.Get("X-Violet-Loop-Detect") == "1" {
|
||||
rw.WriteHeader(http.StatusLoopDetected)
|
||||
_, _ = rw.Write([]byte("Detected a routing loop\n"))
|
||||
return
|
||||
}
|
||||
if req.Header.Get("X-Violet-Raw-Favicon") != "1" {
|
||||
switch req.URL.Path {
|
||||
case "/favicon.svg", "/favicon.png", "/favicon.ico":
|
||||
icons := fav.GetIcons(req.Host)
|
||||
if icons == nil {
|
||||
break
|
||||
}
|
||||
raw, contentType, err := icons.ProduceForExt(path.Ext(req.URL.Path))
|
||||
if err != nil {
|
||||
utils.RespondVioletError(rw, http.StatusTeapot, "No icon available")
|
||||
return
|
||||
}
|
||||
rw.Header().Set("Content-Type", contentType)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write(raw)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
62
servers/https_test.go
Normal file
62
servers/https_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package servers
|
||||
|
||||
import (
|
||||
"github.com/1f349/violet"
|
||||
"github.com/1f349/violet/certs"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/1f349/violet/router"
|
||||
"github.com/1f349/violet/servers/conf"
|
||||
"github.com/1f349/violet/utils/fake"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeTransport struct{}
|
||||
|
||||
func (f *fakeTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
|
||||
rec := httptest.NewRecorder()
|
||||
rec.WriteHeader(http.StatusOK)
|
||||
return rec.Result(), nil
|
||||
}
|
||||
|
||||
func TestNewHttpsServer_RateLimit(t *testing.T) {
|
||||
db, err := violet.InitDB("file:TestNewHttpsServer_RateLimit?mode=memory&cache=shared")
|
||||
assert.NoError(t, err)
|
||||
|
||||
ft := &fakeTransport{}
|
||||
httpsConf := &conf.Conf{
|
||||
RateLimit: 5,
|
||||
Domains: &fake.Domains{},
|
||||
Certs: certs.New(nil, nil, true),
|
||||
Signer: fake.SnakeOilProv,
|
||||
Router: router.NewManager(db, proxy.NewHybridTransportWithCalls(ft, ft, &websocket.Server{})),
|
||||
}
|
||||
srv := NewHttpsServer(httpsConf, nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||
req.RemoteAddr = "127.0.0.1:1447"
|
||||
assert.NoError(t, err)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(5)
|
||||
for i := 0; i < 5; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.Handler.ServeHTTP(rec, req)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTooManyRequests, res.StatusCode)
|
||||
}
|
118
servers/metrics/httpmiddleware.go
Normal file
118
servers/metrics/httpmiddleware.go
Normal file
@ -0,0 +1,118 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Copyright 2022 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package metrics is adapted from
|
||||
// https://github.com/bwplotka/correlator/tree/main/examples/observability/ping/pkg/httpinstrumentation
|
||||
// https://github.com/prometheus/client_golang/blob/main/examples/middleware/httpmiddleware/httpmiddleware.go
|
||||
|
||||
type Middleware interface {
|
||||
// WrapHandler wraps the given HTTP handler for instrumentation.
|
||||
WrapHandler(handlerName string, handler http.Handler) http.HandlerFunc
|
||||
}
|
||||
|
||||
type middleware struct {
|
||||
buckets []float64
|
||||
registry prometheus.Registerer
|
||||
}
|
||||
|
||||
// WrapHandler wraps the given HTTP handler for instrumentation:
|
||||
// It registers four metric collectors (if not already done) and reports HTTP
|
||||
// metrics to the (newly or already) registered collectors.
|
||||
// Each has a constant label named "handler" with the provided handlerName as
|
||||
// value.
|
||||
func (m *middleware) WrapHandler(handlerName string, handler http.Handler) http.HandlerFunc {
|
||||
reg := prometheus.WrapRegistererWith(prometheus.Labels{"handler": handlerName}, m.registry)
|
||||
|
||||
requestsTotal := promauto.With(reg).NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Tracks the number of HTTP requests.",
|
||||
}, []string{"method", "code", "host"},
|
||||
)
|
||||
requestDuration := promauto.With(reg).NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "Tracks the latencies for HTTP requests.",
|
||||
Buckets: m.buckets,
|
||||
},
|
||||
[]string{"method", "code", "host"},
|
||||
)
|
||||
requestSize := promauto.With(reg).NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "http_request_size_bytes",
|
||||
Help: "Tracks the size of HTTP requests.",
|
||||
},
|
||||
[]string{"method", "code", "host"},
|
||||
)
|
||||
responseSize := promauto.With(reg).NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "http_response_size_bytes",
|
||||
Help: "Tracks the size of HTTP responses.",
|
||||
},
|
||||
[]string{"method", "code", "host"},
|
||||
)
|
||||
|
||||
hostCtxGetter := promhttp.WithLabelFromCtx("host", func(ctx context.Context) string {
|
||||
s, _ := ctx.Value(hostCtxKey(0)).(string)
|
||||
return s
|
||||
})
|
||||
|
||||
// Wraps the provided http.Handler to observe the request result with the provided metrics.
|
||||
base := promhttp.InstrumentHandlerCounter(
|
||||
requestsTotal,
|
||||
promhttp.InstrumentHandlerDuration(
|
||||
requestDuration,
|
||||
promhttp.InstrumentHandlerRequestSize(
|
||||
requestSize,
|
||||
promhttp.InstrumentHandlerResponseSize(
|
||||
responseSize,
|
||||
handler,
|
||||
hostCtxGetter,
|
||||
),
|
||||
hostCtxGetter,
|
||||
),
|
||||
hostCtxGetter,
|
||||
),
|
||||
hostCtxGetter,
|
||||
)
|
||||
|
||||
return base.ServeHTTP
|
||||
}
|
||||
|
||||
// New returns a Middleware interface.
|
||||
func New(registry prometheus.Registerer, buckets []float64) Middleware {
|
||||
if buckets == nil {
|
||||
buckets = prometheus.ExponentialBuckets(0.1, 1.5, 5)
|
||||
}
|
||||
|
||||
return &middleware{
|
||||
buckets: buckets,
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
type hostCtxKey uint8
|
||||
|
||||
func AddHostCtx(req *http.Request) *http.Request {
|
||||
return req.WithContext(context.WithValue(req.Context(), hostCtxKey(0), req.Host))
|
||||
}
|
15
sqlc.yaml
Normal file
15
sqlc.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
version: "2"
|
||||
sql:
|
||||
- engine: sqlite
|
||||
queries: database/queries
|
||||
schema: database/migrations
|
||||
gen:
|
||||
go:
|
||||
package: "database"
|
||||
out: "database"
|
||||
emit_json_tags: true
|
||||
overrides:
|
||||
- column: "routes.flags"
|
||||
go_type: "github.com/1f349/violet/target.Flags"
|
||||
- column: "redirects.flags"
|
||||
go_type: "github.com/1f349/violet/target.Flags"
|
42
target/flags.go
Normal file
42
target/flags.go
Normal file
@ -0,0 +1,42 @@
|
||||
package target
|
||||
|
||||
type Flags uint64
|
||||
|
||||
const (
|
||||
FlagPre Flags = 1 << iota
|
||||
FlagAbs
|
||||
FlagCors
|
||||
FlagSecureMode
|
||||
FlagForwardHost
|
||||
FlagForwardAddr
|
||||
FlagIgnoreCert
|
||||
FlagWebsocket
|
||||
)
|
||||
|
||||
var (
|
||||
routeFlagMask = FlagPre | FlagAbs | FlagCors | FlagSecureMode | FlagForwardHost | FlagForwardAddr | FlagIgnoreCert | FlagWebsocket
|
||||
redirectFlagMask = FlagPre | FlagAbs
|
||||
)
|
||||
|
||||
// HasFlag returns true if the bits contain the requested flag
|
||||
func (f Flags) HasFlag(flag Flags) bool {
|
||||
// 0110 & 0100 == 0100 (value != 0 thus true)
|
||||
// 0011 & 0100 == 0000 (value == 0 thus false)
|
||||
return f&flag != 0
|
||||
}
|
||||
|
||||
// NormaliseRouteFlags returns only the bits used for routes
|
||||
func (f Flags) NormaliseRouteFlags() Flags {
|
||||
// removes bits outside the mask
|
||||
// 0110 & 0111 == 0110
|
||||
// 1010 & 0111 == 0010 (values are different)
|
||||
return f & routeFlagMask
|
||||
}
|
||||
|
||||
// NormaliseRedirectFlags returns only the bits used for redirects
|
||||
func (f Flags) NormaliseRedirectFlags() Flags {
|
||||
// removes bits outside the mask
|
||||
// 0110 & 0111 == 0110
|
||||
// 1010 & 0111 == 0010 (values are different)
|
||||
return f & redirectFlagMask
|
||||
}
|
@ -2,7 +2,7 @@ package target
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/1f349/violet/utils"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@ -12,20 +12,31 @@ import (
|
||||
// Redirect is a target used by the router to manage redirecting the request
|
||||
// using the specified configuration.
|
||||
type Redirect struct {
|
||||
Pre bool // if the path has had a prefix removed
|
||||
Host string // target host
|
||||
Port int // target port
|
||||
Path string // target path (possibly a prefix or absolute)
|
||||
Abs bool // if the path is a prefix or absolute
|
||||
Code int // status code used to redirect
|
||||
Src string `json:"src"` // request source
|
||||
Dst string `json:"dst"` // redirect destination
|
||||
Desc string `json:"desc"` // description for admin panel use
|
||||
Flags Flags `json:"flags"` // extra flags
|
||||
Code int64 `json:"code"` // status code used to redirect
|
||||
}
|
||||
|
||||
// FullHost outputs a host:port combo or just the host if the port is 0.
|
||||
func (r Redirect) FullHost() string {
|
||||
if r.Port == 0 {
|
||||
return r.Host
|
||||
type RedirectWithActive struct {
|
||||
Redirect
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (r Redirect) OnDomain(domain string) bool {
|
||||
// if there is no / then the first part is still the domain
|
||||
domainPart, _, _ := strings.Cut(r.Src, "/")
|
||||
if domainPart == domain {
|
||||
return true
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
||||
|
||||
// domainPart could start with a subdomain
|
||||
return strings.HasSuffix(domainPart, "."+domain)
|
||||
}
|
||||
|
||||
func (r Redirect) HasFlag(flag Flags) bool {
|
||||
return r.Flags&flag != 0
|
||||
}
|
||||
|
||||
// ServeHTTP responds with the redirect to the response writer provided.
|
||||
@ -36,10 +47,12 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
code = http.StatusFound
|
||||
}
|
||||
|
||||
// split the host and path
|
||||
host, p := utils.SplitHostPath(r.Dst)
|
||||
|
||||
// if not Abs then join with the ending of the current path
|
||||
p := r.Path
|
||||
if !r.Abs {
|
||||
p = path.Join(r.Path, req.URL.Path)
|
||||
if !r.Flags.HasFlag(FlagAbs) {
|
||||
p = path.Join(p, req.URL.Path)
|
||||
|
||||
// replace the trailing slash that path.Join() strips off
|
||||
if strings.HasSuffix(req.URL.Path, "/") {
|
||||
@ -55,12 +68,17 @@ func (r Redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// create a new URL
|
||||
u := &url.URL{
|
||||
Scheme: req.URL.Scheme,
|
||||
Host: r.FullHost(),
|
||||
Host: host,
|
||||
Path: p,
|
||||
}
|
||||
|
||||
// close the incoming body after use
|
||||
if req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
}
|
||||
|
||||
// use fast redirect for speed
|
||||
utils.FastRedirect(rw, req, u.String(), code)
|
||||
utils.FastRedirect(rw, req, u.String(), int(code))
|
||||
}
|
||||
|
||||
// String outputs a debug string for the redirect.
|
||||
|
@ -7,9 +7,20 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRedirect_FullHost(t *testing.T) {
|
||||
assert.Equal(t, "localhost", Redirect{Host: "localhost"}.FullHost())
|
||||
assert.Equal(t, "localhost:22", Redirect{Host: "localhost", Port: 22}.FullHost())
|
||||
func TestRedirect_OnDomain(t *testing.T) {
|
||||
assert.True(t, Route{Src: "example.com"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "test.example.com"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "example.com/hello"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "test.example.com/hello"}.OnDomain("example.com"))
|
||||
assert.False(t, Route{Src: "example.com"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "test.example.com"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "example.com/hello"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "test.example.com/hello"}.OnDomain("example.org"))
|
||||
}
|
||||
|
||||
func TestRedirect_HasFlag(t *testing.T) {
|
||||
assert.True(t, Route{Flags: FlagPre | FlagAbs}.HasFlag(FlagPre))
|
||||
assert.False(t, Route{Flags: FlagPre | FlagAbs}.HasFlag(FlagCors))
|
||||
}
|
||||
|
||||
func TestRedirect_ServeHTTP(t *testing.T) {
|
||||
@ -17,14 +28,14 @@ func TestRedirect_ServeHTTP(t *testing.T) {
|
||||
Redirect
|
||||
target string
|
||||
}{
|
||||
{Redirect{Host: "example.com", Path: "/bye", Abs: true, Code: http.StatusFound}, "https://example.com/bye"},
|
||||
{Redirect{Host: "example.com", Path: "/bye", Code: http.StatusFound}, "https://example.com/bye/hello/world"},
|
||||
{Redirect{Dst: "example.com/bye", Flags: FlagAbs, Code: http.StatusFound}, "https://example.com/bye"},
|
||||
{Redirect{Dst: "example.com/bye", Code: http.StatusFound}, "https://example.com/bye/hello/world"},
|
||||
}
|
||||
for _, i := range a {
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil)
|
||||
i.ServeHTTP(res, req)
|
||||
assert.Equal(t, i.Code, res.Code)
|
||||
assert.Equal(t, i.Code, int64(res.Code))
|
||||
assert.Equal(t, i.target, res.Header().Get("Location"))
|
||||
}
|
||||
}
|
||||
|
273
target/route.go
273
target/route.go
@ -1,23 +1,29 @@
|
||||
package target
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/MrMelon54/violet/proxy"
|
||||
"github.com/MrMelon54/violet/utils"
|
||||
"github.com/1f349/violet/logger"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/utils"
|
||||
websocket2 "github.com/gorilla/websocket"
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/net/http/httpguts"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Violet Serve Route")
|
||||
|
||||
// serveApiCors outputs the cors headers to make APIs work.
|
||||
var serveApiCors = cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
// allow all origins for api requests
|
||||
AllowOriginFunc: func(origin string) bool { return true },
|
||||
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||
AllowedMethods: []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
@ -26,8 +32,6 @@ var serveApiCors = cors.New(cors.Options{
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodOptions,
|
||||
http.MethodTrace,
|
||||
},
|
||||
AllowCredentials: true,
|
||||
})
|
||||
@ -35,22 +39,33 @@ var serveApiCors = cors.New(cors.Options{
|
||||
// Route is a target used by the router to manage forwarding traffic to an
|
||||
// internal server using the specified configuration.
|
||||
type Route struct {
|
||||
Pre bool // if the path has had a prefix removed
|
||||
Host string // target host
|
||||
Port int // target port
|
||||
Path string // target path (possibly a prefix or absolute)
|
||||
Abs bool // if the path is a prefix or absolute
|
||||
Cors bool // add CORS headers
|
||||
SecureMode bool // use HTTPS internally
|
||||
ForwardHost bool // forward host header internally
|
||||
ForwardAddr bool // forward remote address
|
||||
IgnoreCert bool // ignore self-cert
|
||||
Headers http.Header // extra headers
|
||||
Proxy http.Handler // reverse proxy handler
|
||||
Src string `json:"src"` // request source
|
||||
Dst string `json:"dst"` // proxy destination
|
||||
Desc string `json:"desc"` // description for admin panel use
|
||||
Flags Flags `json:"flags"` // extra flags
|
||||
Headers http.Header `json:"-"` // extra headers
|
||||
Proxy *proxy.HybridTransport `json:"-"` // reverse proxy handler
|
||||
}
|
||||
|
||||
// IsIgnoreCert returns true if IgnoreCert is enabled.
|
||||
func (r Route) IsIgnoreCert() bool { return r.IgnoreCert }
|
||||
type RouteWithActive struct {
|
||||
Route
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (r Route) OnDomain(domain string) bool {
|
||||
// if there is no / then the first part is still the domain
|
||||
domainPart, _, _ := strings.Cut(r.Src, "/")
|
||||
if domainPart == domain {
|
||||
return true
|
||||
}
|
||||
|
||||
// domainPart could start with a subdomain
|
||||
return strings.HasSuffix(domainPart, "."+domain)
|
||||
}
|
||||
|
||||
func (r Route) HasFlag(flag Flags) bool {
|
||||
return r.Flags&flag != 0
|
||||
}
|
||||
|
||||
// UpdateHeaders takes an existing set of headers and overwrites them with the
|
||||
// extra headers.
|
||||
@ -60,18 +75,10 @@ func (r Route) UpdateHeaders(header http.Header) {
|
||||
}
|
||||
}
|
||||
|
||||
// FullHost outputs a host:port combo or just the host if the port is 0.
|
||||
func (r Route) FullHost() string {
|
||||
if r.Port == 0 {
|
||||
return r.Host
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
||||
}
|
||||
|
||||
// ServeHTTP responds with the data proxied from the internal server to the
|
||||
// response writer provided.
|
||||
func (r Route) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if r.Cors {
|
||||
if r.HasFlag(FlagCors) {
|
||||
// wraps with CORS handler
|
||||
serveApiCors.Handler(http.HandlerFunc(r.internalServeHTTP)).ServeHTTP(rw, req)
|
||||
} else {
|
||||
@ -84,21 +91,16 @@ func (r Route) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// set the scheme and port using defaults if the port is 0
|
||||
scheme := "http"
|
||||
if r.SecureMode {
|
||||
if r.HasFlag(FlagSecureMode) {
|
||||
scheme = "https"
|
||||
if r.Port == 0 {
|
||||
r.Port = 443
|
||||
}
|
||||
} else {
|
||||
if r.Port == 0 {
|
||||
r.Port = 80
|
||||
}
|
||||
}
|
||||
|
||||
// split the host and path
|
||||
host, p := utils.SplitHostPath(r.Dst)
|
||||
|
||||
// if not Abs then join with the ending of the current path
|
||||
p := r.Path
|
||||
if !r.Abs {
|
||||
p = path.Join(r.Path, req.URL.Path)
|
||||
if !r.HasFlag(FlagAbs) {
|
||||
p = path.Join(p, req.URL.Path)
|
||||
|
||||
// replace the trailing slash that path.Join() strips off
|
||||
if strings.HasSuffix(req.URL.Path, "/") {
|
||||
@ -111,25 +113,23 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
p = "/"
|
||||
}
|
||||
|
||||
// TODO: don't just copy the body into a buffer as this is really slow
|
||||
buf := new(bytes.Buffer)
|
||||
if req.Body != nil {
|
||||
_, _ = io.Copy(buf, req.Body)
|
||||
}
|
||||
|
||||
// create a new URL
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: r.FullHost(),
|
||||
Host: host,
|
||||
Path: p,
|
||||
RawQuery: req.URL.RawQuery,
|
||||
}
|
||||
|
||||
// close the incoming body after use
|
||||
if req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
}
|
||||
|
||||
// create the internal request
|
||||
req2, err := http.NewRequest(req.Method, u.String(), buf)
|
||||
req2, err := http.NewRequest(req.Method, u.String(), req.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ServeRoute::ServeHTTP()] Error generating new request: %s\n", err)
|
||||
utils.RespondHttpStatus(rw, http.StatusBadGateway)
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "Invalid request for proxy")
|
||||
return
|
||||
}
|
||||
|
||||
@ -153,18 +153,179 @@ func (r Route) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// if forward host is enabled then send the host
|
||||
if r.ForwardHost {
|
||||
if r.HasFlag(FlagForwardHost) {
|
||||
req2.Host = req.Host
|
||||
}
|
||||
if r.ForwardAddr {
|
||||
req2.Header.Add("X-Forwarded-For", req.RemoteAddr)
|
||||
|
||||
// adds extra request metadata
|
||||
if r.internalReverseProxyMeta(rw, req, req2) {
|
||||
return
|
||||
}
|
||||
|
||||
// switch to websocket handler
|
||||
// internally the http hijack method is called
|
||||
if r.HasFlag(FlagWebsocket) && websocket2.IsWebSocketUpgrade(req2) {
|
||||
r.Proxy.ConnectWebsocket(rw, req2)
|
||||
return
|
||||
}
|
||||
|
||||
req2.Header.Set("X-Violet-Loop-Detect", "1")
|
||||
|
||||
// serve request with reverse proxy
|
||||
r.Proxy.ServeHTTP(rw, proxy.SetReverseProxyHost(req2, r))
|
||||
var resp *http.Response
|
||||
if r.HasFlag(FlagIgnoreCert) {
|
||||
resp, err = r.Proxy.InsecureRoundTrip(req2)
|
||||
} else {
|
||||
resp, err = r.Proxy.SecureRoundTrip(req2)
|
||||
}
|
||||
if err != nil {
|
||||
Logger.Warn("Error receiving internal round trip response", "route src", r.Src, "url", req2.URL.String(), "err", err)
|
||||
utils.RespondVioletError(rw, http.StatusBadGateway, "Error receiving internal round trip response")
|
||||
return
|
||||
}
|
||||
|
||||
// make sure to close response body after use
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusLoopDetected {
|
||||
Logger.Warn("Loop Detected", "method", req.Method, "url", req.URL, "url2", req2.URL.String())
|
||||
utils.RespondVioletError(rw, http.StatusLoopDetected, "Error loop detected")
|
||||
return
|
||||
}
|
||||
|
||||
// copy headers and status code
|
||||
copyHeader(rw.Header(), resp.Header)
|
||||
rw.WriteHeader(resp.StatusCode)
|
||||
|
||||
// copy body
|
||||
if resp.Body != nil {
|
||||
_, err := io.Copy(rw, resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// internalReverseProxyMeta is mainly built from code copied from httputil.ReverseProxy,
|
||||
// due to the highly custom nature of this reverse proxy software we use a copy
|
||||
// of the code instead of the full httputil implementation to prevent overhead
|
||||
// from the more generic implementation
|
||||
func (r Route) internalReverseProxyMeta(rw http.ResponseWriter, req, req2 *http.Request) bool {
|
||||
if req.ContentLength == 0 {
|
||||
req2.Body = nil // Issue 16036: nil Body for http.Transport retries
|
||||
}
|
||||
if req2.Header == nil {
|
||||
req2.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate
|
||||
}
|
||||
|
||||
reqUpType := upgradeType(req2.Header)
|
||||
if !asciiIsPrint(reqUpType) {
|
||||
utils.RespondVioletError(rw, http.StatusBadRequest, fmt.Sprintf("Invalid protocol %s", reqUpType))
|
||||
return true
|
||||
}
|
||||
removeHopByHopHeaders(req2.Header)
|
||||
|
||||
// Issue 21096: tell backend applications that care about trailer support
|
||||
// that we support trailers. (We do, but we don't go out of our way to
|
||||
// advertise that unless the incoming client request thought it was worth
|
||||
// mentioning.) Note that we look at req.Header, not outreq.Header, since
|
||||
// the latter has passed through removeHopByHopHeaders.
|
||||
if httpguts.HeaderValuesContainsToken(req.Header["Te"], "trailers") {
|
||||
req2.Header.Set("Te", "trailers")
|
||||
}
|
||||
|
||||
// After stripping all the hop-by-hop connection headers above, add back any
|
||||
// necessary for protocol upgrades, such as for websockets.
|
||||
if reqUpType != "" {
|
||||
req2.Header.Set("Connection", "Upgrade")
|
||||
req2.Header.Set("Upgrade", reqUpType)
|
||||
}
|
||||
|
||||
if r.HasFlag(FlagForwardAddr) {
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
prior, ok := req2.Header["X-Forwarded-For"]
|
||||
omit := ok && prior == nil // Issue 38079: nil now means don't populate the header
|
||||
if len(prior) > 0 {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
if !omit {
|
||||
req2.Header.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// String outputs a debug string for the route.
|
||||
func (r Route) String() string {
|
||||
return fmt.Sprintf("%#v", r)
|
||||
}
|
||||
|
||||
// copyHeader copies all headers from src to dst
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateType returns the value of upgrade from http.Header
|
||||
func upgradeType(h http.Header) string {
|
||||
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
|
||||
return ""
|
||||
}
|
||||
return h.Get("Upgrade")
|
||||
}
|
||||
|
||||
// IsPrint returns whether s is ASCII and printable according to
|
||||
// https://tools.ietf.org/html/rfc20#section-4.2.
|
||||
func asciiIsPrint(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < ' ' || s[i] > '~' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Hop-by-hop headers. These are removed when sent to the backend.
|
||||
// As of RFC 7230, hop-by-hop headers are required to appear in the
|
||||
// Connection header field. These are the headers defined by the
|
||||
// obsoleted RFC 2616 (section 13.5.1) and are used for backward
|
||||
// compatibility.
|
||||
var hopHeaders = []string{
|
||||
"Connection",
|
||||
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Te", // canonicalized version of "TE"
|
||||
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
}
|
||||
|
||||
// removeHopByHopHeaders removes the hop-by-hop headers defined in hopHeaders
|
||||
func removeHopByHopHeaders(h http.Header) {
|
||||
// RFC 7230, section 6.1: Remove headers listed in the "Connection" header.
|
||||
for _, f := range h["Connection"] {
|
||||
for _, sf := range strings.Split(f, ",") {
|
||||
if sf = textproto.TrimString(sf); sf != "" {
|
||||
h.Del(sf)
|
||||
}
|
||||
}
|
||||
}
|
||||
// RFC 2616, section 13.5.1: Remove a set of known hop-by-hop headers.
|
||||
// This behavior is superseded by the RFC 7230 Connection header, but
|
||||
// preserve it for backwards compatibility.
|
||||
for _, f := range hopHeaders {
|
||||
h.Del(f)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
package target
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/1f349/violet/proxy"
|
||||
"github.com/1f349/violet/proxy/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@ -9,19 +14,33 @@ import (
|
||||
|
||||
type proxyTester struct {
|
||||
got bool
|
||||
rw http.ResponseWriter
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
func (p *proxyTester) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
p.got = true
|
||||
p.rw = rw
|
||||
p.req = req
|
||||
func (p *proxyTester) makeHybridTransport() *proxy.HybridTransport {
|
||||
return proxy.NewHybridTransportWithCalls(p, p, &websocket.Server{})
|
||||
}
|
||||
|
||||
func TestRoute_FullHost(t *testing.T) {
|
||||
assert.Equal(t, "localhost", Route{Host: "localhost"}.FullHost())
|
||||
assert.Equal(t, "localhost:22", Route{Host: "localhost", Port: 22}.FullHost())
|
||||
func (p *proxyTester) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
p.got = true
|
||||
p.req = req
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
}
|
||||
|
||||
func TestRoute_OnDomain(t *testing.T) {
|
||||
assert.True(t, Route{Src: "example.com"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "test.example.com"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "example.com/hello"}.OnDomain("example.com"))
|
||||
assert.True(t, Route{Src: "test.example.com/hello"}.OnDomain("example.com"))
|
||||
assert.False(t, Route{Src: "example.com"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "test.example.com"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "example.com/hello"}.OnDomain("example.org"))
|
||||
assert.False(t, Route{Src: "test.example.com/hello"}.OnDomain("example.org"))
|
||||
}
|
||||
|
||||
func TestRoute_HasFlag(t *testing.T) {
|
||||
assert.True(t, Route{Flags: FlagPre | FlagAbs}.HasFlag(FlagPre))
|
||||
assert.False(t, Route{Flags: FlagPre | FlagAbs}.HasFlag(FlagCors))
|
||||
}
|
||||
|
||||
func TestRoute_ServeHTTP(t *testing.T) {
|
||||
@ -29,26 +48,28 @@ func TestRoute_ServeHTTP(t *testing.T) {
|
||||
Route
|
||||
target string
|
||||
}{
|
||||
{Route{Host: "localhost", Port: 1234, Path: "/bye", Abs: true}, "http://localhost:1234/bye"},
|
||||
{Route{Host: "1.2.3.4", Path: "/bye"}, "http://1.2.3.4:80/bye/hello/world"},
|
||||
{Route{Host: "2.2.2.2", Path: "/world", Abs: true, SecureMode: true}, "https://2.2.2.2:443/world"},
|
||||
{Route{Host: "api.example.com", Path: "/world", Abs: true, SecureMode: true, ForwardHost: true}, "https://api.example.com:443/world"},
|
||||
{Route{Host: "api.example.org", Path: "/world", Abs: true, SecureMode: true, ForwardAddr: true}, "https://api.example.org:443/world"},
|
||||
{Route{Host: "3.3.3.3", Path: "/headers", Abs: true, Headers: http.Header{"X-Other": []string{"test value"}}}, "http://3.3.3.3:80/headers"},
|
||||
{Route{Dst: "localhost:1234/bye", Flags: FlagAbs}, "http://localhost:1234/bye"},
|
||||
{Route{Dst: "1.2.3.4/bye"}, "http://1.2.3.4/bye/hello/world"},
|
||||
{Route{Dst: "2.2.2.2/world", Flags: FlagAbs | FlagSecureMode}, "https://2.2.2.2/world"},
|
||||
{Route{Dst: "api.example.com/world", Flags: FlagAbs | FlagSecureMode | FlagForwardHost}, "https://api.example.com/world"},
|
||||
{Route{Dst: "api.example.org/world", Flags: FlagAbs | FlagSecureMode | FlagForwardAddr}, "https://api.example.org/world"},
|
||||
{Route{Dst: "3.3.3.3/headers", Flags: FlagAbs, Headers: http.Header{"X-Other": []string{"test value"}, "X-Violet-Loop-Detect": []string{"1"}}}, "http://3.3.3.3/headers"},
|
||||
}
|
||||
for _, i := range a {
|
||||
pt := &proxyTester{}
|
||||
i.Proxy = pt
|
||||
i.Proxy = pt.makeHybridTransport()
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "https://www.example.com/hello/world", nil)
|
||||
i.ServeHTTP(res, req)
|
||||
|
||||
assert.True(t, pt.got)
|
||||
assert.Equal(t, i.target, pt.req.URL.String())
|
||||
if i.ForwardAddr {
|
||||
assert.Equal(t, req.RemoteAddr, pt.req.Header.Get("X-Forwarded-For"))
|
||||
if i.HasFlag(FlagForwardAddr) {
|
||||
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, host, pt.req.Header.Get("X-Forwarded-For"))
|
||||
}
|
||||
if i.ForwardHost {
|
||||
if i.HasFlag(FlagForwardHost) {
|
||||
assert.Equal(t, req.Host, pt.req.Host)
|
||||
}
|
||||
if i.Headers != nil {
|
||||
@ -62,14 +83,32 @@ func TestRoute_ServeHTTP_Cors(t *testing.T) {
|
||||
res := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodOptions, "https://www.example.com/test", nil)
|
||||
req.Header.Set("Origin", "https://test.example.com")
|
||||
i := &Route{Host: "1.1.1.1", Port: 8080, Path: "/hello", Cors: true, Proxy: pt}
|
||||
i := &Route{Dst: "1.1.1.1:8080/hello", Flags: FlagCors, Proxy: pt.makeHybridTransport()}
|
||||
i.ServeHTTP(res, req)
|
||||
|
||||
assert.True(t, pt.got)
|
||||
assert.Equal(t, http.MethodOptions, pt.req.Method)
|
||||
assert.Equal(t, "http://1.1.1.1:8080/hello/test", pt.req.URL.String())
|
||||
assert.Equal(t, "Origin", res.Header().Get("Vary"))
|
||||
assert.Equal(t, "*", res.Header().Get("Access-Control-Allow-Origin"))
|
||||
assert.Equal(t, "https://test.example.com", res.Header().Get("Access-Control-Allow-Origin"))
|
||||
assert.Equal(t, "true", res.Header().Get("Access-Control-Allow-Credentials"))
|
||||
assert.Equal(t, "Origin", res.Header().Get("Vary"))
|
||||
}
|
||||
|
||||
func TestRoute_ServeHTTP_Body(t *testing.T) {
|
||||
pt := &proxyTester{}
|
||||
res := httptest.NewRecorder()
|
||||
buf := bytes.NewBuffer([]byte{0x54})
|
||||
req := httptest.NewRequest(http.MethodPost, "https://www.example.com/test", buf)
|
||||
req.Header.Set("Origin", "https://test.example.com")
|
||||
i := &Route{Dst: "1.1.1.1:8080/hello", Flags: FlagCors, Proxy: pt.makeHybridTransport()}
|
||||
i.ServeHTTP(res, req)
|
||||
|
||||
assert.True(t, pt.got)
|
||||
assert.Equal(t, http.MethodPost, pt.req.Method)
|
||||
assert.Equal(t, "http://1.1.1.1:8080/hello/test", pt.req.URL.String())
|
||||
all, err := io.ReadAll(pt.req.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, bytes.Compare(all, []byte{0x54}))
|
||||
assert.NoError(t, pt.req.Body.Close())
|
||||
}
|
||||
|
55
utils/acme-challenges.go
Normal file
55
utils/acme-challenges.go
Normal file
@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import "sync"
|
||||
|
||||
type AcmeChallenges struct {
|
||||
s *sync.RWMutex
|
||||
d map[string]*AcmeStorage
|
||||
}
|
||||
|
||||
type AcmeStorage struct {
|
||||
s *sync.RWMutex
|
||||
v map[string]string
|
||||
}
|
||||
|
||||
func NewAcmeChallenge() *AcmeChallenges {
|
||||
return &AcmeChallenges{
|
||||
s: &sync.RWMutex{},
|
||||
d: make(map[string]*AcmeStorage),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AcmeChallenges) Get(domain, key string) string {
|
||||
a.s.RLock()
|
||||
defer a.s.RUnlock()
|
||||
if m := a.d[domain]; m != nil {
|
||||
m.s.RLock()
|
||||
defer m.s.RUnlock()
|
||||
return m.v[key]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AcmeChallenges) Put(domain, key, value string) {
|
||||
a.s.Lock()
|
||||
m := a.d[domain]
|
||||
if m == nil {
|
||||
m = &AcmeStorage{
|
||||
s: &sync.RWMutex{},
|
||||
v: make(map[string]string),
|
||||
}
|
||||
a.d[domain] = m
|
||||
}
|
||||
m.s.Lock()
|
||||
m.v[key] = value
|
||||
m.s.Unlock()
|
||||
a.s.Unlock()
|
||||
}
|
||||
|
||||
func (a *AcmeChallenges) Delete(domain, key string) {
|
||||
a.s.Lock()
|
||||
if m := a.d[domain]; m != nil {
|
||||
delete(m.v, key)
|
||||
}
|
||||
a.s.Unlock()
|
||||
}
|
27
utils/acme-challenges_test.go
Normal file
27
utils/acme-challenges_test.go
Normal file
@ -0,0 +1,27 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAcmeChallenges(t *testing.T) {
|
||||
a := NewAcmeChallenge()
|
||||
assert.Equal(t, "", a.Get("example.com", "123"))
|
||||
|
||||
// The challenge should be created
|
||||
a.Put("example.com", "123", "123abc")
|
||||
assert.Equal(t, "123abc", a.Get("example.com", "123"))
|
||||
|
||||
// The challenge should be deleted
|
||||
a.Delete("example.com", "123")
|
||||
assert.Equal(t, "", a.Get("example.com", "123"))
|
||||
|
||||
// This should not crash or stop execution
|
||||
a.Delete("example.com", "123")
|
||||
assert.Equal(t, "", a.Get("example.com", "123"))
|
||||
|
||||
// This should not crash or stop execution
|
||||
a.Delete("www.example.com", "123")
|
||||
assert.Equal(t, "", a.Get("example.com", "123"))
|
||||
}
|
22
utils/compilable_test.go
Normal file
22
utils/compilable_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeCompile struct{ done bool }
|
||||
|
||||
func (f *fakeCompile) Compile() {
|
||||
f.done = true
|
||||
}
|
||||
|
||||
var _ Compilable = &fakeCompile{}
|
||||
|
||||
func TestMultiCompilable_Compile(t *testing.T) {
|
||||
f := &fakeCompile{}
|
||||
a := MultiCompilable{f}
|
||||
assert.False(t, f.done)
|
||||
a.Compile()
|
||||
assert.True(t, f.done)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@ -27,13 +28,13 @@ func SplitDomainPort(host string, defaultPort int) (domain string, port int, ok
|
||||
// without the port.
|
||||
//
|
||||
// example.com:443 => example.com
|
||||
func GetDomainWithoutPort(domain string) (string, bool) {
|
||||
func GetDomainWithoutPort(domain string) string {
|
||||
// if a valid index isn't found then return false
|
||||
n := strings.LastIndexByte(domain, ':')
|
||||
if n == -1 {
|
||||
return "", false
|
||||
return domain
|
||||
}
|
||||
return domain[:n], true
|
||||
return domain[:n]
|
||||
}
|
||||
|
||||
// ReplaceSubdomainWithWildcard returns the domain with the subdomain replaced
|
||||
@ -65,21 +66,41 @@ func GetParentDomain(domain string) (string, bool) {
|
||||
//
|
||||
// hello.world.example.com => example.com
|
||||
func GetTopFqdn(domain string) (string, bool) {
|
||||
var countDot int
|
||||
n := strings.LastIndexFunc(domain, func(r rune) bool {
|
||||
// return true if this is the second '.'
|
||||
// otherwise counts one and continues
|
||||
if r == '.' {
|
||||
if countDot == 1 {
|
||||
return true
|
||||
}
|
||||
countDot++
|
||||
}
|
||||
return false
|
||||
})
|
||||
// if a valid index isn't found then return false
|
||||
if n == -1 {
|
||||
return "", false
|
||||
}
|
||||
return domain[n+1:], true
|
||||
out, err := publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
return out, err == nil
|
||||
}
|
||||
|
||||
// SplitHostPath extracts the host/path from the input
|
||||
func SplitHostPath(a string) (host, path string) {
|
||||
// check if source has path
|
||||
n := strings.IndexByte(a, '/')
|
||||
if n == -1 {
|
||||
// set host then path to /
|
||||
host = a
|
||||
path = "/"
|
||||
} else {
|
||||
// set host then custom path
|
||||
host = a[:n]
|
||||
path = a[n:] // this required to keep / at the start of the path
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SplitHostPathQuery extracts the host/path?query from the input
|
||||
func SplitHostPathQuery(a string) (host, path, query string) {
|
||||
host, path = SplitHostPath(a)
|
||||
if path == "/" {
|
||||
n := strings.IndexByte(host, '?')
|
||||
if n != -1 {
|
||||
query = host[n+1:]
|
||||
host = host[:n]
|
||||
}
|
||||
return
|
||||
}
|
||||
n := strings.IndexByte(path, '?')
|
||||
if n != -1 {
|
||||
query = path[n+1:]
|
||||
path = path[:n] // reassign happens after
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -18,12 +18,16 @@ func TestSplitDomainPort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDomainWithoutPort(t *testing.T) {
|
||||
domain, ok := GetDomainWithoutPort("www.example.com:5612")
|
||||
assert.True(t, ok, "Output should be true")
|
||||
domain := GetDomainWithoutPort("www.example.com:5612")
|
||||
assert.Equal(t, "www.example.com", domain)
|
||||
|
||||
domain, ok = GetDomainWithoutPort("example.com:443")
|
||||
assert.True(t, ok, "Output should be true")
|
||||
domain = GetDomainWithoutPort("example.com:443")
|
||||
assert.Equal(t, "example.com", domain)
|
||||
|
||||
domain = GetDomainWithoutPort("www.example.com")
|
||||
assert.Equal(t, "www.example.com", domain)
|
||||
|
||||
domain = GetDomainWithoutPort("example.com")
|
||||
assert.Equal(t, "example.com", domain)
|
||||
}
|
||||
|
||||
@ -48,7 +52,11 @@ func TestGetBaseDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTopFqdn(t *testing.T) {
|
||||
domain, ok := GetTopFqdn("www.example.com")
|
||||
domain, ok := GetTopFqdn("example.com")
|
||||
assert.True(t, ok, "Output should be true")
|
||||
assert.Equal(t, "example.com", domain)
|
||||
|
||||
domain, ok = GetTopFqdn("www.example.com")
|
||||
assert.True(t, ok, "Output should be true")
|
||||
assert.Equal(t, "example.com", domain)
|
||||
|
||||
@ -56,3 +64,40 @@ func TestGetTopFqdn(t *testing.T) {
|
||||
assert.True(t, ok, "Output should be true")
|
||||
assert.Equal(t, "example.com", domain)
|
||||
}
|
||||
|
||||
func TestSplitHostPath(t *testing.T) {
|
||||
h, p := SplitHostPath("example.com/hello/world")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/hello/world", p)
|
||||
|
||||
h, p = SplitHostPath("example.com")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/", p)
|
||||
}
|
||||
|
||||
func TestSplitHostPathQuery(t *testing.T) {
|
||||
h, p, q := SplitHostPathQuery("example.com/hello/world")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/hello/world", p)
|
||||
assert.Equal(t, "", q)
|
||||
|
||||
h, p, q = SplitHostPathQuery("example.com")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/", p)
|
||||
assert.Equal(t, "", q)
|
||||
|
||||
h, p, q = SplitHostPathQuery("example.com/hello/world?a=b")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/hello/world", p)
|
||||
assert.Equal(t, "a=b", q)
|
||||
|
||||
h, p, q = SplitHostPathQuery("example.com?a=b")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/", p)
|
||||
assert.Equal(t, "a=b", q)
|
||||
|
||||
h, p, q = SplitHostPathQuery("example.com/?a=b")
|
||||
assert.Equal(t, "example.com", h)
|
||||
assert.Equal(t, "/", p)
|
||||
assert.Equal(t, "a=b", q)
|
||||
}
|
||||
|
11
utils/fake/fake-compilable.go
Normal file
11
utils/fake/fake-compilable.go
Normal file
@ -0,0 +1,11 @@
|
||||
package fake
|
||||
|
||||
import "github.com/1f349/violet/utils"
|
||||
|
||||
// Compilable implements utils.Compilable and stores if the Compile function
|
||||
// is called.
|
||||
type Compilable struct{ Done bool }
|
||||
|
||||
func (f *Compilable) Compile() { f.Done = true }
|
||||
|
||||
var _ utils.Compilable = &Compilable{}
|
13
utils/fake/fake-domains.go
Normal file
13
utils/fake/fake-domains.go
Normal file
@ -0,0 +1,13 @@
|
||||
package fake
|
||||
|
||||
import "github.com/1f349/violet/utils"
|
||||
|
||||
// Domains implements DomainProvider and makes sure `example.com` is valid
|
||||
type Domains struct{}
|
||||
|
||||
func (f *Domains) IsValid(host string) bool { return host == "example.com" }
|
||||
func (f *Domains) Put(string, bool) {}
|
||||
func (f *Domains) Delete(string) {}
|
||||
func (f *Domains) Compile() {}
|
||||
|
||||
var _ utils.DomainProvider = &Domains{}
|
2
utils/fake/fake.go
Normal file
2
utils/fake/fake.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package fake contains fake structs used during tests
|
||||
package fake
|
30
utils/fake/mjwt.go
Normal file
30
utils/fake/mjwt.go
Normal file
@ -0,0 +1,30 @@
|
||||
package fake
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/mjwt/auth"
|
||||
"github.com/1f349/mjwt/claims"
|
||||
"time"
|
||||
)
|
||||
|
||||
var SnakeOilProv = GenSnakeOilProv()
|
||||
|
||||
func GenSnakeOilProv() mjwt.Signer {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return mjwt.NewMJwtSigner("violet.test", key)
|
||||
}
|
||||
|
||||
func GenSnakeOilKey(perm string) string {
|
||||
p := claims.NewPermStorage()
|
||||
p.Set(perm)
|
||||
val, err := SnakeOilProv.GenerateJwt("abc", "abc", nil, 5*time.Minute, auth.AccessTokenClaims{Perms: p})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
}
|
21
utils/interfaces.go
Normal file
21
utils/interfaces.go
Normal file
@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
type DomainProvider interface {
|
||||
IsValid(host string) bool
|
||||
Put(domain string, active bool)
|
||||
Delete(domain string)
|
||||
Compile()
|
||||
}
|
||||
|
||||
type AcmeChallengeProvider interface {
|
||||
Get(domain, key string) string
|
||||
Put(domain, key, value string)
|
||||
Delete(domain, key string)
|
||||
}
|
||||
|
||||
type CertProvider interface {
|
||||
GetCertForDomain(domain string) *tls.Certificate
|
||||
Compile()
|
||||
}
|
@ -7,5 +7,10 @@ import (
|
||||
|
||||
// RespondHttpStatus outputs the status code and text using http.Error()
|
||||
func RespondHttpStatus(rw http.ResponseWriter, status int) {
|
||||
http.Error(rw, fmt.Sprintf("%d %s\n", status, http.StatusText(status)), status)
|
||||
http.Error(rw, fmt.Sprintf("%d %s", status, http.StatusText(status)), status)
|
||||
}
|
||||
|
||||
func RespondVioletError(rw http.ResponseWriter, status int, msg string) {
|
||||
rw.Header().Set("X-Violet-Error", msg)
|
||||
RespondHttpStatus(rw, status)
|
||||
}
|
||||
|
32
utils/response_test.go
Normal file
32
utils/response_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRespondHttpStatus(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
RespondHttpStatus(rec, http.StatusTeapot)
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Equal(t, "418 I'm a teapot", res.Status)
|
||||
a, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "418 I'm a teapot\n", string(a))
|
||||
}
|
||||
|
||||
func TestRespondVioletError(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
RespondVioletError(rec, http.StatusTeapot, "Hidden Error Message")
|
||||
res := rec.Result()
|
||||
assert.Equal(t, http.StatusTeapot, res.StatusCode)
|
||||
assert.Equal(t, "418 I'm a teapot", res.Status)
|
||||
a, err := io.ReadAll(res.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "418 I'm a teapot\n", string(a))
|
||||
assert.Equal(t, "Hidden Error Message", res.Header.Get("X-Violet-Error"))
|
||||
}
|
@ -1,33 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"github.com/charmbracelet/log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// logHttpServerError is the internal function powering the logging in
|
||||
// RunBackgroundHttp and RunBackgroundHttps.
|
||||
func logHttpServerError(prefix string, err error) {
|
||||
func logHttpServerError(logger *log.Logger, err error) {
|
||||
if err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
log.Printf("[%s] The http server shutdown successfully\n", prefix)
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Info("The http server shutdown successfully")
|
||||
} else {
|
||||
log.Printf("[%s] Error trying to host the http server: %s\n", prefix, err.Error())
|
||||
logger.Info("Error trying to host the http server", "err", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunBackgroundHttp runs a http server and logs when the server closes or
|
||||
// errors.
|
||||
func RunBackgroundHttp(prefix string, s *http.Server) {
|
||||
logHttpServerError(prefix, s.ListenAndServe())
|
||||
func RunBackgroundHttp(logger *log.Logger, s *http.Server, ln net.Listener) {
|
||||
logHttpServerError(logger, s.Serve(ln))
|
||||
}
|
||||
|
||||
// RunBackgroundHttps runs a http server with TLS encryption and logs when the
|
||||
// server closes or errors.
|
||||
func RunBackgroundHttps(prefix string, s *http.Server) {
|
||||
logHttpServerError(prefix, s.ListenAndServeTLS("", ""))
|
||||
func RunBackgroundHttps(logger *log.Logger, s *http.Server, ln net.Listener) {
|
||||
logHttpServerError(logger, s.ServeTLS(ln, "", ""))
|
||||
}
|
||||
|
||||
// GetBearer returns the bearer from the Authorization header or an empty string
|
||||
|
20
utils/server-utils_test.go
Normal file
20
utils/server-utils_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetBearer(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Authorization", "Bearer abc")
|
||||
assert.Equal(t, "abc", GetBearer(req))
|
||||
}
|
||||
|
||||
func TestGetBearer_Empty(t *testing.T) {
|
||||
req, err := http.NewRequest(http.MethodPost, "https://example.com", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", GetBearer(req))
|
||||
}
|
77
violet.openapi.yaml
Normal file
77
violet.openapi.yaml
Normal file
@ -0,0 +1,77 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Violet
|
||||
description: Violet
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Webmaster
|
||||
email: webmaster@1f349.net
|
||||
servers:
|
||||
- url: 'https://api.1f349.net/v1/violet'
|
||||
paths:
|
||||
/compile:
|
||||
post:
|
||||
summary: Compile quick access data
|
||||
tags:
|
||||
- compile
|
||||
responses:
|
||||
'202':
|
||||
description: Compile trigger sent
|
||||
/domain/{domain}:
|
||||
put:
|
||||
summary: Add an allowed domain
|
||||
tags:
|
||||
- domain
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
required: true
|
||||
description: The domain to add
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'202':
|
||||
description: Domain added and compiled list reloaded
|
||||
delete:
|
||||
summary: Remove an allowed domain
|
||||
tags:
|
||||
- domain
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
required: true
|
||||
description: The domain to remove
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'202':
|
||||
description: Domain removed and compiled list reloaded
|
||||
/acme-challenge/{domain}/{key}/{value}:
|
||||
put:
|
||||
summary: Add ACME challenge value
|
||||
tags:
|
||||
- acme-challenge
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
required: true
|
||||
description: The domain to add the challenge on
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'202':
|
||||
description: ACME challenge added
|
||||
delete:
|
||||
summary: Add ACME challenge value
|
||||
tags:
|
||||
- acme-challenge
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
required: true
|
||||
description: The domain to add the challenge on
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'202':
|
||||
description: ACME challenge added
|
Loading…
x
Reference in New Issue
Block a user