commit a5890141064a89c15413e73dc5ff22a7b5e08098 Author: bytedream Date: Sun Dec 19 17:30:51 2021 +0100 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd1eb89 --- /dev/null +++ b/Makefile @@ -0,0 +1,121 @@ +VERSION=0.1.0 + +BUILDDIR = . +_BUILDDIR = $(shell realpath $(BUILDDIR))/ + +build: build-server build-container build-extra + +build-server: + cd server/ && go build -o $(_BUILDDIR)/docker4ssh + +build-container: DEBUG=false +build-container: + @if $(DEBUG); then\ + cd container/ && cargo build --target x86_64-unknown-linux-musl --target-dir $(_BUILDDIR) --bin configure;\ + else\ + cd container/ && cargo build --target x86_64-unknown-linux-musl --target-dir $(_BUILDDIR) --release --bin configure;\ + fi + cp -rf $(_BUILDDIR)/x86_64-unknown-linux-musl/$(shell if $(DEBUG); then echo debug; else echo release; fi)/configure $(_BUILDDIR) + +build-extra: SSHPASS:=$(shell LC_ALL=C tr -dc 'A-Za-z0-9!#$%&()*,-./:<=>?@[\]^_{}~' < /dev/urandom | head -c 18 ; echo) +build-extra: + if [ "$(_BUILDDIR)" != "$(shell realpath .)/" ]; then\ + cp -rf LICENSE $(_BUILDDIR)/LICENSE;\ + cp -rf man/ $(_BUILDDIR);\ + fi + yes | ssh-keygen -t ed25519 -f $(_BUILDDIR)/docker4ssh.key -N "$(SSHPASS)" -b 4096 > /dev/null + cp -rf extra/docker4ssh.conf $(_BUILDDIR) + sed -i 's|Passphrase = ""|Passphrase = "$(SSHPASS)"|' $(_BUILDDIR)/docker4ssh.conf + cat extra/database.sql | sqlite3 $(_BUILDDIR)/docker4ssh.sqlite3 + mkdir -p $(_BUILDDIR)/profile/ && cp -f extra/profile.conf $(_BUILDDIR)/profile/ + +optimize: optimize-server optimize-container + +optimize-server: + strip $(_BUILDDIR)/docker4ssh + +optimize-container: + strip $(_BUILDDIR)/configure + +clean: clean-server clean-container clean-extra + +clean-server: + rm -rf $(_BUILDDIR)/docker4ssh + +clean-container: + rm -rf $(_BUILDDIR)/{x86_64-unknown-linux-musl,configure} + +clean-extra: + rm -rf $(_BUILDDIR)/docker4ssh* + rm -rf $(_BUILDDIR)/man/ + rm -rf $(_BUILDDIR)/profile/ + +DESTDIR= +PREFIX=/usr +install: + install -Dm755 $(_BUILDDIR)docker4ssh $(DESTDIR)$(PREFIX)/bin/docker4ssh + install -Dm644 $(_BUILDDIR)LICENSE $(DESTDIR)$(PREFIX)/share/licenses/docker4ssh/LICENSE + install -Dm644 $(_BUILDDIR)man/docker4ssh.1 $(DESTDIR)$(PREFIX)/share/man/man1/docker4ssh.1 + install -Dm644 $(_BUILDDIR)man/docker4ssh.conf.5 $(DESTDIR)$(PREFIX)/share/man/man5/docker4ssh.conf.5 + install -Dm644 $(_BUILDDIR)man/profile.conf.5 $(DESTDIR)$(PREFIX)/share/man/man5/profile.conf.5 + + install -Dm755 $(_BUILDDIR)configure $(DESTDIR)/etc/docker4ssh/configure + install -Dm775 $(_BUILDDIR)docker4ssh.conf $(DESTDIR)/etc/docker4ssh/docker4ssh.conf + install -Dm755 $(_BUILDDIR)docker4ssh.sqlite3 $(DESTDIR)/etc/docker4ssh/docker4ssh.sqlite3 + install -Dm755 $(_BUILDDIR)docker4ssh.key $(DESTDIR)/etc/docker4ssh/docker4ssh.key + install -Dm644 $(_BUILDDIR)man/* -t $(DESTDIR)/etc/docker4ssh/man/ + install -Dm644 $(_BUILDDIR)profile/* -t $(DESTDIR)/etc/docker4ssh/profile/ + install -Dm644 $(_BUILDDIR)LICENSE $(DESTDIR)/etc/docker4ssh/LICENSE + +uninstall: + rm -rf $(DESTDIR)/etc/docker4ssh/ + rm -f $(DESTDIR)$(PREFIX)/bin/docker4ssh + rm -f $(DESTDIR)$(PREFIX)/share/man/man1/docker4ssh.1 + rm -f $(DESTDIR)$(PREFIX)/share/man/man5/{docker4ssh,profile}.5 + rm -f $(DESTDIR)$(PREFIX)/share/licenses/docker4ssh/LICENSE + +release: + mkdir -p /tmp/docker4ssh-$(VERSION)-build/ /tmp/docker4ssh-$(VERSION)-release/ + $(MAKE) BUILDDIR=/tmp/docker4ssh-$(VERSION)-build/ SSHPASS= build optimize + $(MAKE) BUILDDIR=/tmp/docker4ssh-$(VERSION)-build/ DESTDIR=/tmp/docker4ssh-$(VERSION)-release/ install + tar -C /tmp/docker4ssh-$(VERSION)-release/ -czf docker4ssh-$(VERSION).tar.gz . + +RUNDIR=/tmp/docker4ssh + +.PHONY run: +run: + $(MAKE) BUILDDIR=$(RUNDIR) SSHPASS= build + cd $(RUNDIR) && ./docker4ssh + +develop: SERVERSUM = $(shell find server/ -type f -exec md5sum {} + | LC_ALL=C sort | md5sum | cut -d ' ' -f1) +develop: CONTAINERSUM = $(shell find container/src/ -type f -exec md5sum {} + | LC_ALL=C sort | md5sum | cut -d ' ' -f1) +develop: EXTRASUM = $(shell find extra/ -type f -exec md5sum {} + | LC_ALL=C sort | md5sum | cut -d ' ' -f1) +# there is maybe a better way to do this stuff but for the moment this works out +develop: + @if [ ! -d $(RUNDIR) ]; then\ + $(MAKE) BUILDDIR=$(RUNDIR) DEBUG=true SSHPASS= build;\ + if [[ $$? -ne 0 ]]; then exit 2; fi;\ + echo -n $(SERVER) > $(RUNDIR)/SERVERSUM;\ + echo -n $(CLIENTSUM) > $(RUNDIR)/CONTAINERSUM;\ + echo -n $(EXTRASUM) > $(RUNDIR)/EXTRASUM;\ + else\ + if [ "$(shell cat $(RUNDIR)/SERVERSUM)" != "$(SERVERSUM)" ]; then\ + $(MAKE) BUILDDIR=$(RUNDIR) clean-server;\ + $(MAKE) BUILDDIR=$(RUNDIR) build-server;\ + if [[ $$? -ne 0 ]]; then exit 2; fi;\ + echo -n $(SERVERSUM) > $(RUNDIR)/SERVERSUM;\ + fi;\ + if [ "$(shell cat $(RUNDIR)/CONTAINERSUM)" != "$(CONTAINERSUM)" ]; then\ + $(MAKE) BUILDDIR=$(RUNDIR) clean-container;\ + $(MAKE) BUILDDIR=$(RUNDIR) DEBUG=true build-container;\ + if [[ $$? -ne 0 ]]; then exit 2; fi;\ + echo -n $(CONTAINERSUM) > $(RUNDIR)/CONTAINERSUM;\ + fi;\ + if [ "$(shell cat $(RUNDIR)/EXTRASUM)" != "$(EXTRASUM)" ]; then\ + $(MAKE) BUILDDIR=$(RUNDIR) clean-extra;\ + $(MAKE) BUILDDIR=$(RUNDIR) SSHPASS= build-extra;\ + if [[ $$? -ne 0 ]]; then exit 2; fi;\ + echo -n $(EXTRASUM) > $(RUNDIR)/EXTRASUM;\ + fi;\ + fi + cd $(RUNDIR) && LOGGING_LEVEL="debug" ./docker4ssh start diff --git a/README.md b/README.md new file mode 100644 index 0000000..71b5cb3 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# docker4ssh - docker containers and more via ssh + +**docker4ssh** is an ssh server that can create new docker containers and re-login into existing ones. + +

+ + Code size + + + Latest commit + + + Download Badge + + + License + + + Release + + + Discord + +

+ +

+ ✨ Features + Β· + ⌨ Installation + Β· + πŸ–‹οΈ Usage + Β· + βš– License +

+ +**Visit the [wiki](https://github.com/ByteDream/docker4ssh/wiki) to get more information and detailed usage instructions** + +## ✨ Features +- Create containers by images (e.g. `ubuntu:21.04@server`) +- Create specific containers for specific usernames with [profiles](https://github.com/ByteDream/docker4ssh/wiki/Configuration-Files#profileconf) +- Containers are configurable from within +- Re-login into existing containers +- Full use of the docker api (unlike [docker2ssh](https://github.com/moul/ssh2docker), which uses the cli, which theoretically could cause code injection) +- Highly configurable [settings](https://github.com/ByteDream/docker4ssh/wiki/Configuration-Files#docker4sshconf) + +## ⌨️ Installation + +For every install method your OS **must** be linux and docker has to be installed. + +- Download from the latest release (currently only x64 architecture supported) + - Download `docker4ssh-.tar.gz` from the [latest release](https://github.com/ByteDream/docker4ssh/releases/latest) + - Install it + - Into your root directory (recommended) + ```shell + $ sudo tar -xvzf docker4ssh-.tar.gz -C / + ``` + - To the same directory + ```shell + $ sudo tar -xvzf docker4ssh-.tar.gz + ``` +- Building from source + + Before start installing, make sure you have to following things ready: + - [Go](https://go.dev/) installed + - [Rust](https://www.rust-lang.org/) installed + - [Make](https://www.gnu.org/software/make/) installed - optional, but highly recommended since we use `make` in the further instructions + + To install docker4ssh, just execute the following commands + ```shell + $ git clone https://github.com/ByteDream/docker4ssh + $ cd docker4ssh + $ make install + ``` + +- Install it from the [AUR](https://aur.archlinux.org/packages/docker4ssh/) (if you're using arch or an arch based distro) + ```shell + $ yay -S docker4ssh + ``` + +## πŸ–‹ Usage + +To start the docker4ssh server, simply type +```shell +$ docker4ssh start +``` + +The default port for the ssh server is 2222, if you want to change it take a look at the [config file](https://github.com/ByteDream/docker4ssh/wiki/docker4ssh.conf). +Dynamic profile generation is enabled by default, so you can start right away. +Type the following to generate a new ubuntu container and connect to it: +```shell +$ ssh -p 2222 ubuntu:latest@127.0.0.1 +``` +You will get a password prompt then where you can type in anything since by default any password is correct. +If you typed in a password, the docker container gets created and the ssh connection is "redirected" to the containers' tty: +```shell +ubuntu:latest@127.0.0.1's password: +β”Œβ”€β”€β”€Container────────────────┐ +β”‚ Container ID: e0f3d48217da β”‚ +β”‚ Network Mode: Host β”‚ +β”‚ Configurable: true β”‚ +β”‚ Run Level: User β”‚ +β”‚ Exit After: β”‚ +β”‚ Keep On Exit: false β”‚ +└──────────────Informationβ”€β”€β”€β”˜ +root@e0f3d48217da:/# +``` + +For further information, visit the [wiki](https://github.com/ByteDream/docker4ssh/wiki). + +## βš– License + +This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0) - see the [LICENSE](LICENSE) file for more details. \ No newline at end of file diff --git a/container/Cargo.lock b/container/Cargo.lock new file mode 100644 index 0000000..5440df3 --- /dev/null +++ b/container/Cargo.lock @@ -0,0 +1,412 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "docker4ssh" +version = "0.1.0" +dependencies = [ + "failure", + "log", + "serde", + "serde_json", + "serde_repr", + "structopt", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e466864e431129c7e0d3476b92f20458e5879919a0596c6472738d9fa2d342f8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9788f4202aa75c240ecc9c15c65185e6a39ccdeb0fd5d008b98825464c87c" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/container/Cargo.toml b/container/Cargo.toml new file mode 100644 index 0000000..bfae00e --- /dev/null +++ b/container/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "docker4ssh" +version = "0.1.0" +edition = "2021" +authors = ["ByteDream"] +repository = "https://github.com/ByteDream/docker4ssh" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "configure" +path = "src/configure/main.rs" + +[dependencies] +failure = "0.1" +log = "0.4" +structopt = "0.3" +serde = { version = "1.0", features = ["derive"]} +serde_json = "1.0" +serde_repr = "0.1" + +[profile.release] +lto = true +opt-level = "z" +panic = "abort" diff --git a/container/src/configure/cli/cli.rs b/container/src/configure/cli/cli.rs new file mode 100644 index 0000000..d9432fc --- /dev/null +++ b/container/src/configure/cli/cli.rs @@ -0,0 +1,296 @@ +use std::fmt::{Debug, format}; +use std::net::TcpStream; +use std::os::unix::process::ExitStatusExt; +use std::process::{Command, ExitStatus}; +use std::time::SystemTime; +use log::{info, warn}; +use structopt::StructOpt; +use structopt::clap::AppSettings; +use crate::configure::cli::parser; +use crate::shared::api::api::API; +use crate::shared::api::request; +use crate::shared::api::request::{ConfigGetResponse, ConfigNetworkMode, ConfigPostRequest, ConfigRunLevel}; + +type Result = std::result::Result; + +trait Execute { + fn execute(self, api: &mut API) -> Result<()>; +} + +#[derive(StructOpt)] +#[structopt( + name = "configure", + about = "A command line wrapper to control docker4ssh containers from within them", + settings = &[AppSettings::ArgRequiredElseHelp] +)] +struct Opts { + #[structopt(short, long, global = true, help = "Verbose output")] + verbose: bool, + + #[structopt(subcommand)] + commands: Option +} + +#[derive(StructOpt)] +#[structopt( + name = "ping", + about = "Ping the control socket" +)] +struct Ping {} + +impl Execute for Ping { + fn execute(self, api: &mut API) -> Result<()> { + let start = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_nanos(); + let result = request::PingRequest::new().request(api)?; + info!("Pong! Ping is {:.4}ms", ((result.received - start) as f64) / 1000.0 / 1000.0); + Ok(()) + } +} + +#[derive(StructOpt)] +#[structopt( + name = "error", + about = "Example error message sent from socket", +)] +struct Error {} + +impl Execute for Error { + fn execute(self, api: &mut API) -> Result<()> { + request::ErrorRequest::new().request(api)?; + Ok(()) + } +} + +#[derive(StructOpt)] +#[structopt( + name = "info", + about = "Shows information about the current container", +)] +struct Info {} + +impl Execute for Info { + fn execute(self, api: &mut API) -> Result<()> { + let result = request::InfoRequest:: new().request(api)?; + info!(concat!( + "\tContainer ID: {}" + ), result.container_id); + Ok(()) + } +} + +#[derive(StructOpt)] +#[structopt( + name = "config", + about = "Get or set the behavior of the current container", + settings = &[AppSettings::ArgRequiredElseHelp] +)] +struct Config { + #[structopt(subcommand)] + commands: Option +} + +#[derive(StructOpt)] +enum ConfigCommands { + Get(ConfigGet), + Set(ConfigSet) +} + +#[derive(StructOpt)] +#[structopt( + name = "get", + about = "Show the current container behavior" +)] +struct ConfigGet {} + +impl Execute for ConfigGet { + fn execute(self, api: &mut API) -> Result<()> { + let response: ConfigGetResponse = request::ConfigGetRequest::new().request(api)?; + + info!(concat!( + "\tNetwork Mode: {}\n", + "\tConfigurable: {}\n", + "\tRun Level: {}\n", + "\tStartup Information: {}\n", + "\tExit After: {}\n", + "\tKeep On Exit: {}" + ), response.network_mode, response.configurable, response.run_level, response.startup_information, response.exit_after, response.keep_on_exit); + + Ok(()) + } +} + +#[derive(StructOpt)] +#[structopt( + name = "set", + about = "Set the current container behavior", + settings = &[AppSettings::ArgRequiredElseHelp] +)] +struct ConfigSet { + #[structopt(long, help = "If the container should keep running even after the user exits", parse(try_from_str = parser::parse_network_mode))] + network_mode: Option, + + #[structopt(long, help = "If the container should be configurable from within")] + configurable: Option, + + #[structopt(long, help = "Set the container stop behavior", parse(try_from_str = parser::parse_config_run_level))] + run_level: Option, + + #[structopt(long, help = "If information about the container should be shown when a user connects")] + startup_information: Option, + + #[structopt(long, help = "Process name after which the container should exit")] + exit_after: Option, + + #[structopt(long, help = "If the container should be not deleted after exit")] + keep_on_exit: Option +} + +impl Execute for ConfigSet { + fn execute(self, api: &mut API) -> Result<()> { + let mut request = request::ConfigPostRequest::new(); + + if let Some(exit_after) = self.exit_after.as_ref() { + let program_runs = Command::new("pidof") + .arg("-s") + .arg(exit_after).status().unwrap().success(); + if !program_runs { + warn!("NOTE: There is currently no process running with the name '{}'", exit_after); + } + } + + request.body.network_mode = self.network_mode; + request.body.configurable = self.configurable; + request.body.run_level = self.run_level; + request.body.startup_information = self.startup_information; + request.body.exit_after = self.exit_after; + request.body.keep_on_exit = self.keep_on_exit; + + request.request(api)?; + + if let Some(keep_on_exit) = self.keep_on_exit { + if keep_on_exit { + if let Ok(auth) = request::AuthGetRequest::new().request(api) { + info!("To reconnect to this container, use the user '{}' for the ssh connection", &auth.user) + } + } + } + + Ok(()) + } +} + +#[derive(StructOpt)] +#[structopt( + name = "auth", + about = "Get or set the container authentication", + settings = &[AppSettings::ArgRequiredElseHelp] +)] +struct Auth { + #[structopt(subcommand)] + commands: Option +} + +#[derive(StructOpt)] +enum AuthCommands { + Get(AuthGet), + Set(AuthSet) +} + +#[derive(StructOpt)] +#[structopt( + name = "get", + about = "Show the current username used for ssh authentication and if a password is set" +)] +struct AuthGet {} + +impl Execute for AuthGet { + fn execute(self, api: &mut API) -> Result<()> { + let response = request::AuthGetRequest::new().request(api)?; + + info!(concat!( + "\tUser: {}\n", + "\tHas Password: {}\n" + ), response.user, response.has_password); + + Ok(()) + } +} + +#[derive(StructOpt)] + #[structopt( + name = "set", + about = "Set the authentication settings", + settings = &[AppSettings::ArgRequiredElseHelp] +)] +struct AuthSet { + #[structopt(long, help = "The container username")] + user: Option, + #[structopt(long, help = "The container password. If empty, the authentication gets removed")] + password: Option +} + +impl Execute for AuthSet { + fn execute(self, api: &mut API) -> Result<()> { + let mut request = request::AuthPostRequest::new(); + request.body.user = self.user; + request.body.password = self.password.clone(); + + request.request(api)?; + + if let Some(password) = self.password { + if password == "" { + warn!("No password was specified so the authentication got deleted") + } + } + + Ok(()) + } +} + +#[derive(StructOpt)] +enum Root { + Auth(Auth), + Error(Error), + Info(Info), + Ping(Ping), + Config(Config) +} + +pub fn cli(route: String) { + if let Some(subcommand) = Opts::from_args().commands { + let mut result: Result<()> = Ok(()); + let mut api = API::new(route, String::new()); + match subcommand { + Root::Auth(auth) => { + if let Some(subsubcommand) = auth.commands { + match subsubcommand { + AuthCommands::Get(auth_get) => { + result = auth_get.execute(&mut api) + } + AuthCommands::Set(auth_set) => { + result = auth_set.execute(&mut api) + } + } + } + }, + Root::Error(error) => result = error.execute(&mut api), + Root::Info(info) => result = info.execute(&mut api), + Root::Ping(ping) => result = ping.execute(&mut api), + Root::Config(config) => { + if let Some(subsubcommand) = config.commands { + match subsubcommand { + ConfigCommands::Get(config_get) => { + result = config_get.execute(&mut api) + } + ConfigCommands::Set(config_set) => { + result = config_set.execute(&mut api) + } + } + } + } + } + if result.is_err() { + log::error!("{}", result.err().unwrap().to_string()) + } + } +} diff --git a/container/src/configure/cli/mod.rs b/container/src/configure/cli/mod.rs new file mode 100644 index 0000000..8e72be7 --- /dev/null +++ b/container/src/configure/cli/mod.rs @@ -0,0 +1,4 @@ +mod cli; +pub mod parser; + +pub use cli::cli; diff --git a/container/src/configure/cli/parser.rs b/container/src/configure/cli/parser.rs new file mode 100644 index 0000000..e9c6c59 --- /dev/null +++ b/container/src/configure/cli/parser.rs @@ -0,0 +1,23 @@ +use std::f32::consts::E; +use std::fmt::format; +use crate::shared::api::request::{ConfigNetworkMode, ConfigRunLevel}; + +pub fn parse_network_mode(src: &str) -> Result { + match String::from(src).to_lowercase().as_str() { + "off" | "1" => Ok(ConfigNetworkMode::Off), + "full" | "2" => Ok(ConfigNetworkMode::Full), + "host" | "3" => Ok(ConfigNetworkMode::Host), + "docker" | "4" => Ok(ConfigNetworkMode::Docker), + "none" | "5" => Ok(ConfigNetworkMode::None), + _ => Err(format!("'{} is not a valid network mode. Choose from 'off', 'full', 'host', 'docker', 'none'", src)) + } +} + +pub fn parse_config_run_level(src: &str) -> Result { + match String::from(src).to_lowercase().as_str() { + "user" | "1" => Ok(ConfigRunLevel::User), + "container" | "2" => Ok(ConfigRunLevel::Container), + "forever" | "3" => Ok(ConfigRunLevel::Forever), + _ => Err(format!("'{}' is not a valid run level. Choose from: 'user', 'container', 'forever'", src)) + } +} diff --git a/container/src/configure/main.rs b/container/src/configure/main.rs new file mode 100644 index 0000000..740155f --- /dev/null +++ b/container/src/configure/main.rs @@ -0,0 +1,19 @@ +use std::fs; +use std::net::TcpStream; +use std::os::unix::net::UnixStream; +use std::process::exit; +use log::{LevelFilter, trace, warn, info, error}; +use docker4ssh::configure::cli; +use docker4ssh::shared::logging::init_logger; + +fn main() { + init_logger(LevelFilter::Debug); + + match fs::read_to_string("/etc/docker4ssh") { + Ok(route) => cli(route), + Err(e) => { + error!("Failed to read /etc/docker4ssh: {}", e.to_string()); + exit(1); + } + } +} diff --git a/container/src/configure/mod.rs b/container/src/configure/mod.rs new file mode 100644 index 0000000..6aef1fb --- /dev/null +++ b/container/src/configure/mod.rs @@ -0,0 +1,3 @@ +pub mod cli; + +pub use cli::cli; diff --git a/container/src/lib.rs b/container/src/lib.rs new file mode 100644 index 0000000..69aa364 --- /dev/null +++ b/container/src/lib.rs @@ -0,0 +1,2 @@ +pub mod shared; +pub mod configure; \ No newline at end of file diff --git a/container/src/shared/api/api.rs b/container/src/shared/api/api.rs new file mode 100644 index 0000000..af76b39 --- /dev/null +++ b/container/src/shared/api/api.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::TcpStream; +use log::Level::Error; +use serde::Deserialize; + +pub type Result = std::result::Result; + +pub struct API { + route: String, + host: String, +} + +impl API { + pub const fn new(route: String, host: String) -> Self { + API { + route, + host, + } + } + + pub fn new_connection(&mut self) -> Result { + match TcpStream::connect(&self.route) { + Ok(stream) => Ok(stream), + Err(e) => Err(failure::format_err!("Failed to connect to {}: {}", self.route, e.to_string())) + } + } + + pub fn request(&mut self, request: &Request) -> Result { + let mut connection = self.new_connection()?; + + connection.write_all(request.as_string().as_bytes())?; + let mut buf: String = String::new(); + connection.read_to_string(&mut buf).map_err(|e| failure::err_msg(e.to_string()))?; + Ok(APIResult::new(request, buf)) + } + + pub fn request_with_err(&mut self, request: &Request) -> Result { + let result = self.request(request)?; + if result.result_code >= 400 { + let err: APIError = result.body()?; + Err(failure::err_msg(format!("Error {}: {}", result.result_code, err.message))) + } else { + Ok(result) + } + } +} + +#[derive(Deserialize)] +pub struct APIError { + message: String +} + +pub struct APIResult { + // TODO: Store the whole request instead of only the path + request_path: String, + + result_code: i32, + result_body: String +} + +impl APIResult { + fn new(request: &Request, raw_response: String) -> Self { + APIResult { + request_path: request.path.clone(), + + // TODO: Parse http body better + result_code: raw_response[9..12].parse().unwrap(), + result_body: raw_response.split_once("\r\n\r\n").unwrap().1.to_string() + } + } + + pub fn path(self) -> String { + self.request_path + } + + pub fn code(&self) -> i32 { + return self.result_code + } + + pub fn has_body(&self) -> bool { + self.result_body.len() > 0 + } + + pub fn body<'a, T: Deserialize<'a>>(&'a self) -> Result { + let result: T = serde_json::from_str(&self.result_body).map_err(|e| { + // checks if the error has a body and if so, return it + if self.has_body() { + let error: APIError = serde_json::from_str(&self.result_body).unwrap_or_else(|ee| { + APIError{message: format!("could not deserialize response: {}", e.to_string())} + }); + failure::format_err!("Failed to call '{}': {}", self.request_path, error.message) + } else { + failure::format_err!("Failed to call '{}': {}", self.request_path, e.to_string()) + } + })?; + Ok(result) + } +} + +pub enum Method { + GET, + POST +} + +pub struct Request { + method: Method, + path: String, + headers: HashMap, + body: String, +} + +impl Request { + pub fn new(path: String) -> Self { + Request{ + method: Method::GET, + path, + headers: Default::default(), + body: "".to_string(), + } + } + + pub fn set_method(&mut self, method: Method) -> &Self { + self.method = method; + self + } + + pub fn set_path(&mut self, path: String) -> &Self { + self.path = path; + self + } + + pub fn set_header(&mut self, field: &str, value: String) -> &Self { + self.headers.insert(String::from(field), value); + self + } + + pub fn set_body(&mut self, body: String) -> &Self { + self.body = body; + self.headers.insert("Content-Length".to_string(), self.body.len().to_string()); + self + } + + pub fn as_string(&self) -> String { + let method; + match self.method { + Method::GET => method = "GET", + Method::POST => method = "POST" + } + + let headers_as_string = self.headers.iter().map(|f| format!("{}: {}", f.0, f.1)).collect::(); + + return format!("{} {} HTTP/1.0\r\n\ + {}\r\n\r\n\ + {}\r\n", method, self.path, headers_as_string, self.body) + } +} diff --git a/container/src/shared/api/mod.rs b/container/src/shared/api/mod.rs new file mode 100644 index 0000000..09d6a88 --- /dev/null +++ b/container/src/shared/api/mod.rs @@ -0,0 +1,2 @@ +pub mod request; +pub mod api; diff --git a/container/src/shared/api/request.rs b/container/src/shared/api/request.rs new file mode 100644 index 0000000..75e20c1 --- /dev/null +++ b/container/src/shared/api/request.rs @@ -0,0 +1,220 @@ +use std::fmt::{Display, Formatter}; +use serde::{Deserialize, Serialize}; +use serde::de::Unexpected::Str; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::shared::api::api::{API, Method, Request, Result}; +use crate::shared::api::api::Method::POST; + +#[derive(Deserialize)] +pub struct PingResponse { + pub received: u128 +} + +pub struct PingRequest { + request: Request +} + +impl PingRequest { + pub fn new() -> Self { + PingRequest { + request: Request::new(String::from("/ping")) + } + } + pub fn request(&self, api: &mut API) -> Result { + let result: PingResponse = api.request_with_err(&self.request)?.body()?; + Ok(result) + } +} + +pub struct ErrorRequest { + request: Request +} + +impl ErrorRequest { + pub fn new() -> Self { + ErrorRequest { + request: Request::new(String::from("/error")) + } + } + + pub fn request(&self, api: &mut API) -> Result<()> { + api.request_with_err(&self.request)?.body()?; + // should never call Ok + Ok(()) + } +} + +#[derive(Deserialize)] +pub struct InfoResponse { + pub container_id: String +} + +pub struct InfoRequest { + request: Request +} + +impl InfoRequest { + pub fn new() -> Self { + InfoRequest{ + request: Request::new(String::from("/info")) + } + } + + pub fn request(&self, api: &mut API) -> Result { + let result: InfoResponse = api.request_with_err(&self.request)?.body()?; + Ok(result) + } +} + +#[derive(Debug, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ConfigRunLevel { + User = 1, + Container = 2, + Forever = 3 +} + +impl Display for ConfigRunLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum ConfigNetworkMode { + Off = 1, + Full = 2, + Host = 3, + Docker = 4, + None = 5 +} + +impl Display for ConfigNetworkMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Deserialize)] +pub struct ConfigGetResponse { + pub network_mode: ConfigNetworkMode, + pub configurable: bool, + pub run_level: ConfigRunLevel, + pub startup_information: bool, + pub exit_after: String, + pub keep_on_exit: bool +} + +pub struct ConfigGetRequest { + request: Request +} + +impl ConfigGetRequest { + pub fn new() -> ConfigGetRequest { + ConfigGetRequest{ + request: Request::new(String::from("/config")) + } + } + + pub fn request(&self, api: &mut API) -> Result { + let result: ConfigGetResponse = api.request_with_err(&self.request)?.body()?; + Ok(result) + } +} + +#[derive(Serialize)] +pub struct ConfigPostBody { + pub network_mode: Option, + pub configurable: Option, + pub run_level: Option, + pub startup_information: Option, + pub exit_after: Option, + pub keep_on_exit: Option +} + +pub struct ConfigPostRequest { + request: Request, + pub body: ConfigPostBody +} + +impl ConfigPostRequest { + pub fn new() -> ConfigPostRequest { + let mut request = Request::new(String::from("/config")); + request.set_method(Method::POST); + + ConfigPostRequest { + request, + body: ConfigPostBody{ + network_mode: None, + configurable: None, + run_level: None, + startup_information: None, + exit_after: None, + keep_on_exit: None + } + } + } + + pub fn request(&mut self, api: &mut API) -> Result<()> { + self.request.set_body(serde_json::to_string(&self.body)?); + api.request_with_err(&self.request)?; + Ok(()) + } +} + +#[derive(Deserialize)] +pub struct AuthGetResponse { + pub user: String, + pub has_password: bool +} + +pub struct AuthGetRequest { + request: Request +} + +impl AuthGetRequest { + pub fn new() -> AuthGetRequest { + AuthGetRequest{ + request: Request::new(String::from("/auth")) + } + } + + pub fn request(&self, api: &mut API) -> Result { + let result: AuthGetResponse = api.request_with_err(&self.request)?.body()?; + Ok(result) + } +} + +#[derive(Serialize)] +pub struct AuthPostBody { + pub user: Option, + pub password: Option +} + +pub struct AuthPostRequest { + request: Request, + pub body: AuthPostBody +} + +impl AuthPostRequest { + pub fn new() -> AuthPostRequest { + let mut request = Request::new(String::from("/auth")); + request.set_method(POST); + + AuthPostRequest { + request, + body: AuthPostBody{ + user: None, + password: None + } + } + } + + pub fn request(&mut self, api: &mut API) -> Result<()> { + self.request.set_body(serde_json::to_string(&self.body)?); + api.request_with_err(&self.request)?; + Ok(()) + } +} diff --git a/container/src/shared/logging/logger.rs b/container/src/shared/logging/logger.rs new file mode 100644 index 0000000..83ccc71 --- /dev/null +++ b/container/src/shared/logging/logger.rs @@ -0,0 +1,19 @@ +use log::{info, Metadata, Record}; + +pub struct Logger; + +impl log::Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("{}", record.args().to_string()) + } + } + + fn flush(&self) { + todo!() + } +} diff --git a/container/src/shared/logging/mod.rs b/container/src/shared/logging/mod.rs new file mode 100644 index 0000000..686e189 --- /dev/null +++ b/container/src/shared/logging/mod.rs @@ -0,0 +1,11 @@ +use log::{LevelFilter, SetLoggerError}; + +pub mod logger; + +pub use logger::Logger; + +static LOGGER: Logger = Logger; + +pub fn init_logger(level: LevelFilter) -> Result<(), SetLoggerError> { + log::set_logger(&Logger).map(|()| log::set_max_level(level)) +} diff --git a/container/src/shared/mod.rs b/container/src/shared/mod.rs new file mode 100644 index 0000000..f19a4cc --- /dev/null +++ b/container/src/shared/mod.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod logging; diff --git a/examples/Dockerfile b/examples/Dockerfile new file mode 100644 index 0000000..d1c55f2 --- /dev/null +++ b/examples/Dockerfile @@ -0,0 +1,47 @@ +FROM golang:1.17 as server + +WORKDIR /docker4ssh + +COPY ["../", "."] + +RUN apt update && \ + apt install make sqlite3 && \ + apt clean && \ + apt autoremove && \ + rm -rf /var/lib/apt/lists/* + +RUN make BUILDDIR=build/ build-server + + +FROM rust:1.56 as client + +WORKDIR /docker4ssh + +COPY ../ . + +RUN apt update && \ + apt install make \ + +RUN make BUILDDIR=build/ build-client + + +FROM alpine:lastest as extra + +WORKDIR /docker4ssh + +COPY ../ . + +RUN apk add make + +RUN make BUILDDIR=build/ build-extra + + +FROM alpine:latest + +WORKDIR /docker4ssh + +COPY --from=server /docker4ssh/build/* . +COPY --from=client /docker4ssh/build/docker4ssh . +COPY --from=extra /docker4ssh/build/* . + +ENTRYPOINT docker4ssh diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 0000000..27be371 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' + +services: + docker4ssh: + build: . + ports: + - "8642:8642" + volumes: + - "./docker4ssh.log.log:/docker4ssh/docker4ssh.log" + restart: unless-stopped + container_name: docker4ssh diff --git a/examples/docker4ssh.service b/examples/docker4ssh.service new file mode 100644 index 0000000..a4de1b4 --- /dev/null +++ b/examples/docker4ssh.service @@ -0,0 +1,14 @@ +[Unit] +Description= +After=network.target docker.service +StartLimitBurst=3 +StartLimitIntervalSec=60 + +[Service] +Type=simple +WorkingDirectory=/etc/docker4ssh +ExecStart=/usr/bin/docker4ssh +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/extra/database.sql b/extra/database.sql new file mode 100644 index 0000000..847ec0f --- /dev/null +++ b/extra/database.sql @@ -0,0 +1,28 @@ +create table if not exists auth +( + container_id text not null, + user text, + password blob +); + +create unique index if not exists auth_container_id_uindex + on auth (container_id); + +create table if not exists settings +( + container_id text not null, + network_mode enum default 3 not null, + configurable bool default 0 not null, + run_level enum default 1 not null, + startup_information bool default 1 not null, + exit_after text default '' not null, + keep_on_exit bool default 0 not null, + check (configurable IN (0, 1)), + check (keep_on_exit IN (0, 1)), + check (network_mode IN (1, 2, 3, 4, 5)), + check (run_level IN (1, 2, 3)), + check (startup_information IN (0, 1)) +); + +create unique index if not exists settings_container_id_uindex + on settings (container_id); diff --git a/extra/docker4ssh.conf b/extra/docker4ssh.conf new file mode 100644 index 0000000..2a134f2 --- /dev/null +++ b/extra/docker4ssh.conf @@ -0,0 +1,57 @@ +[profile] +# the directory where profiles are stored +Dir = "./profile/" + +# defalt settings for profiles +[profile.default] +Password = "" +NetworkMode = 3 +Configurable = true +RunLevel = 1 +StartupInformation = true +ExitAfter = "" +KeepOnExit = false + +# settings for dynamic container creation +[profile.dynamic] +Enable = true +Password = "" +NetworkMode = 3 +Configurable = true +RunLevel = 1 +StartupInformation = true +ExitAfter = "" +KeepOnExit = false + +[api] +Port = 8420 + +[api.configure] +Binary = "./configure" +Man = "./man/configure.1" + +[ssh] +# the default ssh port. if blank, port 2222 will be used +Port = 2222 +# path to the ssh private key. if blank, a random key will be generated +Keyfile = "./docker4ssh.key" +# password of the ssh private key +Passphrase = "" + +[database] +# path to sqlite3 database file. there may be support for other databases in the future +Sqlite3File = "./docker4ssh.sqlite3" + +[network.default] +Subnet = "172.69.0.0/16" + +[network.isolate] +Subnet = "172.96.0.0/16" + +[logging] +# the loglevel. available levels are: debug, info, warn, error, fatal +Level = "info" +ConsoleOutput = true +ConsoleError = true +OutputFile = "./docker4ssh.log" +ErrorFile = "./docker4ssh.log" diff --git a/extra/profile.conf b/extra/profile.conf new file mode 100644 index 0000000..8b8f03f --- /dev/null +++ b/extra/profile.conf @@ -0,0 +1,33 @@ +#[chad] +# REQUIRED - (ssh) username. can be specified via regex. +# to use regex, put a 'regex:' in front of it +# Username = "chad" + +# OPTIONAL - (ssh) password. can be specified via regex or as hash. +# to use regex, put a 'regex:' in front of it. +# if you want to specify a hash, put a 'sha1:', 'sha256:' or 'sha512:' at the begging of it +# Password = "" + +# OPTIONAL - the network mode. must be one of the following: 1 (off) | 2 (isolate) | 3 (host) | 4 (docker) | 5 (none) +# NetworkMode = 3 + +# OPTIONAL - if the container should be configurable +# Configurable = true + +# OPTIONAL - the container run behavior. must be one of the following: 1 (user) | 2 (container) 3 | (forever) +# RunLevel = 1 + +# OPTIONAL - if information should be shown about the container on startup +# StartupInformation = true + +# OPTIONAL - a process name to exit after it has finished +# ExitAfter = "" + +# OPTIONAL - not delete the container when it stops working +# KeepOnExit = false + +# REQUIRED OR `Container` - the image to connect to +# Image = "archlinux:latest" + +# REQUIRED OR `Image` - the container id to connect to +# Container = "" diff --git a/man/configure.1 b/man/configure.1 new file mode 100644 index 0000000..df079a3 --- /dev/null +++ b/man/configure.1 @@ -0,0 +1,96 @@ +.TH configure 1 "December 13, 2021" configure "configure - manage docker4ssh container from within" + +.SH NAME +docker4ssh - docker containers and more via ssh + +.SH AUTH GET +This can only be used when calling \fIauth get\fR. +.br +It returns the current username (with which you can login to the container), if a password is set and if the container is reachable for other ssh connections. + +.SH AUTH SET +This can only be used when calling \fIauth set\fR. +.TP + +\fB--user\fR = user +The container username. It is used if you want to (re)connect to the container. +.TP + +\fB--password\fR = password +The container password. If empty, the authentication gets removed. + +.SH ERROR +This can only be used when calling \fIerror\fR. +.br +The subcommand only exists for testing purposes and always return a 400 error. + +.SH INFO +This can only be used when calling \fIinfo\fR. +.br +It returns info about the container. Currently only the full container id is shown. + +.SH PING +This can only be used when calling \fIping\fR. +.br +It returns the ping to the docker4ssh host with a nice little message :) + +.SH CONFIG GET +This can only be used when calling \fIconfig get\fR. +.br +It returns the container configuration with the details NetworkMode, Configurable, RunLevel, ExitAfter and KeepOnExit (\fIprofile.conf (5)\fR). + +.SH CONFIG SET +This can only be used when calling \fIconfig set\fR. +.TP + +\fB--configurable\fI = true | false +If the container should be configurable (calling the binary this manual is about). +Once called this can only be reverted when the database is edited manually. +.TP + +\fB--exit-after\fR = exit after +Process name to stop the container after the process ends. +.TP + +\fB--keep-on-exit\fR = true | false +If the container should or should not be deleted when it stops working. +.TP + +\fB--network-mode\fR = 1 | 2 | 3 | 4 | 5 +This describes the behavior of the container's network +Must be one of the following: + 1 (Off): Disable networking complete. + 2 (Isolate): Isolates the container from the host and the host's network. Therefore, no configurations can be changed from within the container. + 3 (Host): Default docker network. + 4 (Docker): Same as \fI3\fR but the container is in a docker4ssh controlled subnet. This is useful to differ normal from docker4ssh containers. + 5 (None): disables all isolation between the docker container and the host, so inside the network the container can act as the host. So it has access to the host's network directly. +.TP + +\fB--run-level\fR = 1 | 2 | 3 +This describes the container behavior when the user connection to a container is stopped. +Must be one of the following: + 1 (User): The container stops working if no user is connected to it anymore. + 2 (Container): The container runs when no user is connected \fIExitAfter\fR is specified. + 3 (Forever): The container runs forever. + +.SH BUGS +Discovered a bug? Well then it should get fixed as fast as possible. Feel free to open a new issue (https://github.com/ByteDream/crunchyroll-go/docker4ssh) or create a pull request (https://github.com/ByteDream/docker4ssh/pulls) on github. + +.SH AUTHOR +Written by ByteDream (https://github.com/ByteDream) + +.SH COPYRIGHT +Copyright (C) 2021 ByteDream + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/man/docker4ssh.1 b/man/docker4ssh.1 new file mode 100644 index 0000000..c25ed89 --- /dev/null +++ b/man/docker4ssh.1 @@ -0,0 +1,36 @@ +.TH docker4ssh 1 "December 13, 2021" docker4ssh "docker4ssh" + +.SH NAME +docker4ssh - docker containers and more via ssh + +.SH FILES +/etc/docker4ssh/docker4ssh.conf + The configuration file. See \fIdocker4ssh.conf(5)\fR for more information + +/etc/docker4ssh/profile/* + Directory containing profiles. See \fIprofile.conf(5)\fR for more information + +.SH SEE ALSO +docker4ssh.conf(5), profile.conf(5) + +.SH BUGS +Discovered a bug? Well then it should get fixed as fast as possible. Feel free to open a new issue (https://github.com/ByteDream/crunchyroll-go/docker4ssh) or create a pull request (https://github.com/ByteDream/docker4ssh/pulls) on github. + +.SH AUTHOR +Written by ByteDream (https://github.com/ByteDream) + +.SH COPYRIGHT +Copyright (C) 2021 ByteDream + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/man/docker4ssh.conf.5 b/man/docker4ssh.conf.5 new file mode 100644 index 0000000..b770274 --- /dev/null +++ b/man/docker4ssh.conf.5 @@ -0,0 +1,169 @@ +.TH docker4ssh.conf 5 "December 13, 2021" docker4ssh.conf "docker4ssh configuration file" + +.SH SYNOPSIS +.TP +/etc/docker4ssh/docker4ssh.conf + +.SH PROFILE +\fBDir\fR = /path/to/directory +.TP +Set the path to the directory where profiles are stored in + +.SH PROFILE.DEFAULT +.TP +\fBPassword\fR = password +Default password for every connection. +This is used unless some other password was specified. +The password can be specified as plain text, regex or hash: + Regex: Put \fIregex:\fR in front of it. The regex must must be \fBgo\fR / \fBgolang\fR compatible. Visit \fIregex101.com\fR to validate your regex. + Hash: Put \fIsha1:\fR, \fIsha256:\fR or \fIsha512:\fR in front of it. Note that the hash must be hashed with the prefix algorithm. +.TP + +\fBNetworkMode\fR = 1 | 2 | 3 | 4 | 5 +Default network mode for every connection. +NetworkMode describes the behavior of the container's network +Must be one of the following: + 1 (Off): Disable networking complete. + 2 (Isolate): Isolates the container from the host and the host's network. Therefore, no configurations can be changed from within the container. + 3 (Host): Default docker network. + 4 (Docker): Same as \fI3\fR but the container is in a docker4ssh controlled subnet. This is useful to differ normal from docker4ssh containers. + 5 (None): disables all isolation between the docker container and the host, so inside the network the container can act as the host. So it has access to the host's network directly. +.TP + +\fBConfigurable\fR = true | false +Default configurable setting for every connection. +Configurable describes if the container should be configurable from within it. This means that the connect user is able to change all settings which are described here. +Must be true or false. +.TP + +\fBRunLevel\fR = 1 | 2 | 3 +Default run level for every connection. +RunRevel describes the container behavior when the user connection to a container is stopped. +Must be one of the following: + 1 (User): The container stops working if no user is connected to it anymore. + 2 (Container): The container runs when no user is connected \fIExitAfter\fR is specified. + 3 (Forever): The container runs forever. +.br +Note that the container exits always, independent of its RunLevel, when the via \fIExitAfter\fR specified process ends. +.TP + +\fBStartupInformation\fR = true | false +Default startup information setting for every connection. +StartupInformation specifies if information about the container (id, network mode, ...) should be shown when a user connects to it. +Must be true or false. +.TP + +\fBExitAfter\fR = exit after +Default exit after process for every process. +ExitAfter is a process name after which end the container should stop running. +.TP + +\fBKeepOnExit\fR = true | false +Default keep on exit setting for every connection. +KeepOnExit specifies if the container should be saved when it stops working. +Must be true or false. + +.SH PROFILE.DYNAMIC +.TP +\fBEnable\fR = true | false +If dynamic container creation should be created. +.TP + +\fBPassword\fR = password +See \fIPROFILE.DEFAULT.Password\fR +.TP + +\fBNetworkMode\fR = 1 | 2 | 3 | 4 | 5 +See \fIPROFILE.DEFAULT.NetworkMode\fR +.TP + +\fBConfigurable\fR = true | false +See \fIPROFILE.DEFAULT.Configurable\fR +.TP + +\fBRunLevel\fR = 1 | 2 | 3 +See \fIPROFILE.DEFAULT.RunLevel\fR +.TP + +\fBStartupInformation\fR = true | false +See \fIPROFILE.DEFAULT.StartupInformation\fR +.TP + +\fBExitAfter\fR = exit after +See \fIPROFILE.DEFAULT.ExitAfter\fR +.TP + +\fBKeepOnExit\fR = true | false +See \fIPROFILE.DEFAULT.KeepOnExit\fR + +.SH API +.TP +\fBPort\fR = port +The api port for container clients to communicate with the server. +.TP + +\fBConfigureBinary\fR = /path/to/configure/binary +Path to the configure binary which is used inside of containers to communicate with the host and configure itself. + +.SH SSH +.TP +\fBPort\fR = port +Port of the ssh server to serve. +.TP + +\fBKey\fR = /path/to/ssh/key +Path to the ssh private key for the ssh server. + +To generate a new ssh key, use: + >>> ssh-keygen -t ed25519 -b 4096 +.TP + +\fBPassword\fR = password +Password for the ssh private key. + +.SH DATABASE +.TP +\fBSqlite3File\fR = /path/to/sqlite3/file +Path of the database file where all container specific configurations are stored in. + +.SH NETWORK +.TP + +.SH NETWORK.DEFAULT +.TP +\fBSubnet\fR = subnet.ip +Ip and mask of the subnet which is used for \fINetworkMode 4 (Docker)\fR. +.TP + +.SH NETWORK.ISOLATE +.TP +\fBSubnet\fR = subnet.ip +Ip and mask of the subnet which is used for \fINetworkMode 2 (Isolate)\fR. +.TP + +.SH LOGGING +.TP +\fBLevel\fR = debug | info | warn | error | fatal +Logging level. +.TP + +\fBConsoleOutput\fR = bool +If normal output should be logged to the console. +.TP + +\fBConsoleError\fR = bool +If error output should be logged to the console. +.TP + +\fBOutputFile\fR = /path/to/output/file +Path to the output file. +.TP + +\fBErrorFile\fR = /path/to/error/file +Path to the error file.an + +.SH SEE ALSO +docker4ssh(1), profile.conf(5) + +.SH AUTHORS +Written by ByteDream (https://github.com/ByteDream) diff --git a/man/profile.conf.5 b/man/profile.conf.5 new file mode 100644 index 0000000..b5810c8 --- /dev/null +++ b/man/profile.conf.5 @@ -0,0 +1,81 @@ +.TH docker4ssh.conf 5 "December 13, 2021" docker4ssh.conf "docker4ssh configuration file" + +.SH SYNOPSIS +.TP +/etc/docker4ssh/profile/* + +.SH SECTION NAME +.TP +A representative name for the profile + +.SH KEYS +\fBUsername\fR = username +Username for this profile. +The username can be specified as plain text or regex: + Regex: Put \fIregex:\fR in front of it. The regex must must be \fBgo\fR / \fBgolang\fR compatible. Visit \fIregex101.com\fR to validate your regex. +.TP + +.TP +\fBPassword\fR = password +Password for the profile. +The password can be specified as plain text, regex or hash: + Regex: Put \fIregex:\fR in front of it. The regex must must be \fBgo\fR / \fBgolang\fR compatible. Visit \fIregex101.com\fR to validate your regex. + Hash: Put \fIsha1:\fR, \fIsha256:\fR or \fIsha512:\fR in front of it. Note that the hash must be hashed with the prefix algorithm. +.TP + +\fBNetworkMode\fR = 1 | 2 | 3 | 4 | 5 +Default network mode for every connection. +NetworkMode describes the behavior of the container's network +Must be one of the following: + 1 (Off): Disable networking complete. + 2 (Isolate): Isolates the container from the host and the host's network. Therefore, no configurations can be changed from within the container. + 3 (Host): Default docker network. + 4 (Docker): Same as \fI3\fR but the container is in a docker4ssh controlled subnet. This is useful to differ normal from docker4ssh containers. + 5 (None): disables all isolation between the docker container and the host, so inside the network the container can act as the host. So it has access to the host's network directly. +.TP + +\fBConfigurable\fR = true | false +Default configurable setting for every connection. +Configurable describes if the container should be configurable from within it. This means that the connect user is able to change all settings which are described here. +Must be true or false. +.TP + +\fBRunLevel\fR = 1 | 2 | 3 +Default run level for every connection. +RunRevel describes the container behavior when the user connection to a container is stopped. +Must be one of the following: + 1 (User): The container stops working if no user is connected to it anymore. + 2 (Container): The container runs when no user is connected \fIExitAfter\fR is specified. + 3 (Forever): The container runs forever. +.br +Note that the container exits always, independent of its RunLevel, when the via \fIExitAfter\fR specified process ends. +.TP + +\fBStartupInformation\fR = true | false +Default startup information setting for every connection. +StartupInformation specifies if information about the container (id, network mode, ...) should be shown when a user connects to it. +Must be true or false. +.TP + +\fBExitAfter\fR = exit after +Default exit after process for every process. +ExitAfter is a process name after which end the container should stop running. +.TP + +\fBKeepOnExit\fR = true | false +Default keep on exit setting for every connection. +KeepOnExit specifies if the container should be saved when it stops working. +Must be true or false. + +.SH EXAMPLE +[test] +.br +Username = "test" +.br +Image = "alpine:latest" + +.SH SEE ALSO +docker4ssh(1), docker4ssh.conf(5) + +.SH AUTHORS +Written by ByteDream (https://github.com/ByteDream) diff --git a/protocol/configure.yaml b/protocol/configure.yaml new file mode 100644 index 0000000..0b15573 --- /dev/null +++ b/protocol/configure.yaml @@ -0,0 +1,203 @@ +openapi: 3.0.1 +info: + title: docker4ssh + description: Communicate between a container and the docker4ssh host + version: 0.1.0 + license: + name: GNU Affero General Public License v3.0 + url: https://www.gnu.org/licenses/agpl-3.0.txt + contact: + name: ByteDream + url: https://github.com/ByteDream +servers: + - url: 'unix:///var/run/docker4ssh.sock' +paths: + /ping: + get: + summary: Ping the server to see if it's latency and if it's alive + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + received: + type: integer + description: Unix nano timestamp when the message was received + /error: + get: + summary: Sends an error with code 400, only for test purposes + responses: + 400: + description: Controlled bad return code + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Example error message + /info: + get: + summary: Get information about the current container + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + container_id: + type: string + description: ID of the container + /config: + get: + summary: Get the configuration of the current container + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + network_mode: + type: integer + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + description: The container network mode. Take a look at server/docker/docker.go for extended information + configurable: + type: boolean + description: If the container should be configurable from within + run_level: + type: integer + enum: + - 1 + - 2 + - 3 + description: The container run level / behavior. Take a look at server/docker/docker.go for extended information + startup_information: + type: boolean + description: If information about the container should be shown when a user connects + exit_after: + type: string + description: The process name after the container exits + keep_on_exit: + type: boolean + description: If the container should be not deleted after exit + post: + summary: Set some config settings + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + network_mode: + type: integer + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + description: The container network mode. Take a look at server/docker/docker.go for extended information + configurable: + type: boolean + description: If the container should be configurable from within + run_level: + type: integer + enum: + - 1 + - 2 + - 3 + description: The container run level / behavior. Take a look at server/docker/docker.go for extended information + startup_information: + type: boolean + description: If information about the container should be shown when a user connects + exit_after: + type: string + description: The process name after the container exits + keep_on_exit: + type: boolean + description: If the container should be not deleted after exit + responses: + 200: + description: Settings was made + 406: + description: One or more settings could not be changed + content: + application/json: + schema: + type: object + properties: + message: + type: string + description: Human readable description why the changes could not be made + rejected: + type: array + description: The rejected changes + items: + type: object + description: The rejected setting + a description why it couldn't be processed + properties: + name: + type: string + description: Name of the setting + description: + type: string + description: Description of the processing error + + /auth: + get: + summary: Returns the current username used for ssh authentication and if a password is set + responses: + 200: + description: OK + content: + application/json: + schema: + type: object + properties: + user: + type: string + description: Username + has_password: + type: boolean + description: If a password is set + 404: + description: Auth does not exist + content: + application/json: + schema: + type: string + description: Message that the auth does not exists + post: + summary: Changes authentication for the current container + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + user: + type: string + description: The new username. Cannot be empty but nullable + password: + type: string + description: The new password. If empty or null, the complete authentication gets deleted + responses: + 200: + description: Configuration was changed + 406: + description: The given username was empty diff --git a/server/api/api.go b/server/api/api.go new file mode 100644 index 0000000..5f5ee10 --- /dev/null +++ b/server/api/api.go @@ -0,0 +1,123 @@ +package api + +import ( + "bytes" + "docker4ssh/config" + "docker4ssh/ssh" + "encoding/json" + "fmt" + "go.uber.org/zap" + "io" + "io/ioutil" + "net" + "net/http" + "strings" +) + +type EndpointHandler struct { + http.Handler + + auth bool + + get func(http.ResponseWriter, *http.Request, *ssh.User) (interface{}, int) + post func(http.ResponseWriter, *http.Request, *ssh.User) (interface{}, int) +} + +func (h *EndpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ip := strings.Split(r.RemoteAddr, ":")[0] + + zap.S().Infof("User connected to api with remote address %s", ip) + + w.Header().Add("Content-Type", "application/json") + + user := ssh.GetUser(ip) + // checks if auth should be checked and if so and no user could be found, response an error + if h.auth && user == nil { + zap.S().Errorf("Could not find api user with ip %s", ip) + json.NewEncoder(w).Encode(APIError{Message: "unauthorized"}) + return + } else if user != nil { + zap.S().Debugf("API ip %s is %s", ip, user.ID) + } + + raw := bytes.Buffer{} + if r.ContentLength > 0 { + io.Copy(&raw, r.Body) + defer r.Body.Close() + if !json.Valid(raw.Bytes()) { + zap.S().Errorf("API user %s sent invalid body", ip) + w.WriteHeader(http.StatusNotAcceptable) + json.NewEncoder(w).Encode(APIError{Message: "invalid body"}) + return + } + r.Body = ioutil.NopCloser(&raw) + } + + zap.S().Debugf("API user %s request - \"%s %s %s\" \"%s\" \"%s\"", ip, r.Method, r.URL.Path, r.Proto, r.UserAgent(), raw.String()) + + var response interface{} + var code int + + switch r.Method { + case http.MethodGet: + if h.get != nil { + response, code = h.get(w, r, user) + } + case http.MethodPost: + if h.post != nil { + response, code = h.post(w, r, user) + } + } + + if response == nil && code == 0 { + zap.S().Infof("API user %s sent invalid method: %s", ip, r.Method) + response = APIError{Message: fmt.Sprintf("invalid method '%s'", r.Method)} + code = http.StatusConflict + } else { + zap.S().Infof("API user %s issued %s successfully", ip, r.URL.Path) + } + + w.WriteHeader(code) + if response != nil { + json.NewEncoder(w).Encode(response) + } +} + +func ServeAPI(config *config.Config) (errChan chan error, closer func() error) { + errChan = make(chan error, 1) + + mux := http.NewServeMux() + + mux.Handle("/ping", &EndpointHandler{ + get: PingGet, + }) + mux.Handle("/error", &EndpointHandler{ + get: ErrorGet, + }) + mux.Handle("/info", &EndpointHandler{ + get: InfoGet, + auth: true, + }) + mux.Handle("/config", &EndpointHandler{ + get: ConfigGet, + post: ConfigPost, + auth: true, + }) + mux.Handle("/auth", &EndpointHandler{ + get: AuthGet, + post: AuthPost, + auth: true, + }) + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.Api.Port)) + if err != nil { + errChan <- err + return + } + + go func() { + errChan <- http.Serve(listener, mux) + }() + + return errChan, listener.Close +} diff --git a/server/api/auth.go b/server/api/auth.go new file mode 100644 index 0000000..36961cf --- /dev/null +++ b/server/api/auth.go @@ -0,0 +1,80 @@ +package api + +import ( + "docker4ssh/database" + "docker4ssh/ssh" + "encoding/json" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" + "net/http" +) + +type authGetResponse struct { + User string `json:"user"` + HasPassword bool `json:"has_password"` +} + +func AuthGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + auth, ok := database.GetDatabase().GetAuthByContainer(user.Container.FullContainerID) + + if ok { + return authGetResponse{ + User: *auth.User, + HasPassword: auth.Password != nil, + }, http.StatusOK + } else { + return APIError{Message: "no auth is set"}, http.StatusNotFound + } +} + +type authPostRequest struct { + User *string `json:"user"` + Password *string `json:"password"` +} + +func AuthPost(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + var request authPostRequest + json.NewDecoder(r.Body).Decode(&request) + defer r.Body.Close() + + db := database.GetDatabase() + + auth, _ := db.GetAuthByContainer(user.Container.FullContainerID) + + if request.User != nil { + if *request.User == "" { + return APIError{Message: "new username cannot be empty"}, http.StatusNotAcceptable + } + if err := db.SetAuth(user.Container.FullContainerID, database.Auth{ + User: request.User, + }); err != nil { + zap.S().Errorf("Error while updating user for user %s: %v", user.ID, err) + return APIError{Message: "failed to process user"}, http.StatusInternalServerError + } + zap.S().Infof("Updated password for %s", user.Container.ContainerID) + } + if request.Password != nil && *request.Password == "" { + if err := db.DeleteAuth(user.Container.FullContainerID); err != nil { + zap.S().Errorf("Error while deleting auth for user %s: %v", user.ID, err) + return APIError{Message: "failed to delete auth"}, http.StatusInternalServerError + } + zap.S().Infof("Deleted authenticiation for %s", user.Container.ContainerID) + } else if request.Password != nil { + pwd, err := bcrypt.GenerateFromPassword([]byte(*request.Password), bcrypt.DefaultCost) + if err != nil { + zap.S().Errorf("Error while updating password for user %s: %v", user.ID, err) + return APIError{Message: "failed to process password"}, http.StatusInternalServerError + } + var username string + if auth.User == nil { + username = user.Container.FullContainerID + } else { + username = *auth.User + } + if err = db.SetAuth(user.Container.FullContainerID, database.NewUnsafeAuth(username, pwd)); err != nil { + return APIError{Message: "failed to update authentication"}, http.StatusInternalServerError + } + zap.S().Infof("Updated password for %s", user.Container.ContainerID) + } + return nil, http.StatusOK +} diff --git a/server/api/config.go b/server/api/config.go new file mode 100644 index 0000000..17e14c5 --- /dev/null +++ b/server/api/config.go @@ -0,0 +1,124 @@ +package api + +import ( + "context" + "docker4ssh/docker" + "docker4ssh/ssh" + "encoding/json" + "fmt" + "go.uber.org/zap" + "net/http" + "reflect" + "strings" +) + +type configGetResponse struct { + NetworkMode docker.NetworkMode `json:"network_mode"` + Configurable bool `json:"configurable"` + RunLevel docker.RunLevel `json:"run_level"` + StartupInformation bool `json:"startup_information"` + ExitAfter string `json:"exit_after"` + KeepOnExit bool `json:"keep_on_exit"` +} + +func ConfigGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + config := user.Container.Config() + + return configGetResponse{ + config.NetworkMode, + config.Configurable, + config.RunLevel, + config.StartupInformation, + config.ExitAfter, + config.KeepOnExit, + }, http.StatusOK +} + +type configPostRequest configGetResponse + +var configPostRequestLookup, _ = structJsonLookup(configPostRequest{}) + +type configPostResponse struct { + Message string `json:"message"` + Rejected []configPostResponseRejected `json:"rejected"` +} + +type configPostResponseRejected struct { + Name string `json:"name"` + Description string `json:"description"` +} + +func ConfigPost(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + var requestBody map[string]interface{} + json.NewDecoder(r.Body).Decode(&requestBody) + defer r.Body.Close() + + var change bool + var response configPostResponse + + updatedConfig := user.Container.Config() + + for k, v := range requestBody { + if v == nil { + continue + } + + kind, ok := configPostRequestLookup[k] + if !ok { + response.Rejected = append(response.Rejected, configPostResponseRejected{ + Name: k, + Description: fmt.Sprintf("name / field %s does not exist", k), + }) + } else { + valueKind := reflect.TypeOf(v).Kind() + if valueKind != kind && valueKind == reflect.Float64 && kind == reflect.Int { + valueKind = reflect.Int + } + + if valueKind != kind { + response.Rejected = append(response.Rejected, configPostResponseRejected{ + Name: k, + Description: fmt.Sprintf("value should be type %s, got type %s", kind, valueKind), + }) + } + + change = true + switch k { + case "network_mode": + updatedConfig.NetworkMode = docker.NetworkMode(v.(float64)) + case "configurable": + updatedConfig.Configurable = v.(bool) + case "run_level": + updatedConfig.RunLevel = docker.RunLevel(v.(float64)) + case "startup_information": + updatedConfig.StartupInformation = v.(bool) + case "exit_after": + updatedConfig.ExitAfter = v.(string) + case "keep_on_exit": + updatedConfig.KeepOnExit = v.(bool) + } + } + } + + if len(response.Rejected) > 0 { + var arr []string + for _, rejected := range response.Rejected { + arr = append(arr, rejected.Name) + } + + if len(response.Rejected) == 1 { + response.Message = fmt.Sprintf("1 invalid configuration was found: %s", strings.Join(arr, ", ")) + return response, http.StatusNotAcceptable + } else if len(response.Rejected) > 1 { + response.Message = fmt.Sprintf("%d invalid configurations were found: %s", len(response.Rejected), strings.Join(arr, ", ")) + return response, http.StatusNotAcceptable + } + } else if change { + if err := user.Container.UpdateConfig(context.Background(), updatedConfig); err != nil { + zap.S().Errorf("Error while updating config for API user %s: %v", user.ID, err) + response.Message = "Internal error while updating the config" + return response, http.StatusInternalServerError + } + } + return nil, http.StatusOK +} diff --git a/server/api/error.go b/server/api/error.go new file mode 100644 index 0000000..fe34620 --- /dev/null +++ b/server/api/error.go @@ -0,0 +1,12 @@ +package api + +import ( + "docker4ssh/ssh" + "net/http" +) + +type errorGetResponse APIError + +func ErrorGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + return APIError{Message: "Example error message"}, http.StatusBadRequest +} diff --git a/server/api/info.go b/server/api/info.go new file mode 100644 index 0000000..412bba8 --- /dev/null +++ b/server/api/info.go @@ -0,0 +1,16 @@ +package api + +import ( + "docker4ssh/ssh" + "net/http" +) + +type infoGetResponse struct { + ContainerID string `json:"container_id"` +} + +func InfoGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + return infoGetResponse{ + ContainerID: user.Container.FullContainerID, + }, http.StatusOK +} diff --git a/server/api/ping.go b/server/api/ping.go new file mode 100644 index 0000000..f8203d4 --- /dev/null +++ b/server/api/ping.go @@ -0,0 +1,15 @@ +package api + +import ( + "docker4ssh/ssh" + "net/http" + "time" +) + +type pingGetResponse struct { + Received int64 `json:"received"` +} + +func PingGet(w http.ResponseWriter, r *http.Request, user *ssh.User) (interface{}, int) { + return pingGetResponse{Received: time.Now().UnixNano()}, http.StatusOK +} diff --git a/server/api/utils.go b/server/api/utils.go new file mode 100644 index 0000000..6f63b2a --- /dev/null +++ b/server/api/utils.go @@ -0,0 +1,35 @@ +package api + +import ( + "fmt" + "reflect" + "strings" +) + +type APIError struct { + Message string `json:"message"` +} + +func structJsonLookup(v interface{}) (map[string]reflect.Kind, error) { + rt := reflect.TypeOf(v) + if rt.Kind() != reflect.Struct { + return nil, fmt.Errorf("given interface is not a struct") + } + + lookup := make(map[string]reflect.Kind) + + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + + name := strings.Split(field.Tag.Get("json"), ",")[0] + value := field.Type.Kind() + + if field.Type.Kind() == reflect.Struct { + value = reflect.Map + } + + lookup[name] = value + } + + return lookup, nil +} diff --git a/server/build/docker4ssh b/server/build/docker4ssh new file mode 100644 index 0000000..e69de29 diff --git a/server/cmd/cmd.go b/server/cmd/cmd.go new file mode 100644 index 0000000..4b3da4e --- /dev/null +++ b/server/cmd/cmd.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "os" +) + +var rootCmd = &cobra.Command{ + Use: "docker4ssh", + Short: "Docker containers and more via ssh", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + } +} diff --git a/server/cmd/start.go b/server/cmd/start.go new file mode 100644 index 0000000..6ce9122 --- /dev/null +++ b/server/cmd/start.go @@ -0,0 +1,160 @@ +package cmd + +import ( + "docker4ssh/api" + c "docker4ssh/config" + "docker4ssh/database" + "docker4ssh/docker" + "docker4ssh/logging" + "docker4ssh/ssh" + "docker4ssh/validate" + "fmt" + "github.com/spf13/cobra" + "go.uber.org/zap" + "os" + "os/signal" + "strings" + "syscall" + "time" +) + +var startCmd = &cobra.Command{ + Use: "start", + Short: "Starts the docker4ssh server", + Args: cobra.MaximumNArgs(0), + + PreRunE: func(cmd *cobra.Command, args []string) error { + return preStart() + }, + Run: func(cmd *cobra.Command, args []string) { + start() + }, +} + +func preStart() error { + if !docker.IsRunning() { + return fmt.Errorf("docker daemon is not running") + } + + cli, err := docker.InitCli() + if err != nil { + return err + } + + config, err := c.InitConfig(true) + if err != nil { + return err + } + + validator := validate.NewConfigValidator(cli, false, config) + + if result := validator.ValidateLogging(); !result.Ok() { + return fmt.Errorf(result.String()) + } + + level := zap.NewAtomicLevel() + level.UnmarshalText([]byte(config.Logging.Level)) + var outputFiles, errorFiles []string + if config.Logging.ConsoleOutput { + outputFiles = append(outputFiles, "/dev/stdout") + } + if config.Logging.OutputFile != "" { + outputFiles = append(outputFiles, config.Logging.OutputFile) + } + if config.Logging.ConsoleError { + errorFiles = append(errorFiles, "/dev/stderr") + } + if config.Logging.ErrorFile != "" { + errorFiles = append(errorFiles, config.Logging.ErrorFile) + } + logging.InitLogging(level, outputFiles, errorFiles) + + if result := validator.Validate(); !result.Ok() { + return fmt.Errorf(result.String()) + } + c.SetConfig(config) + + db, err := database.NewSqlite3Connection(config.Database.Sqlite3File) + if err != nil { + zap.S().Fatalf("Failed to initialize database: %v", err) + } + database.SetDatabase(db) + + return nil +} + +func start() { + config := c.GetConfig() + + if config.SSH.Passphrase == "" { + zap.S().Warn("YOU HAVE AN EMPTY PASSPHRASE WHICH IS INSECURE, SUGGESTING CREATING A NEW SSH KEY WITH A PASSPHRASE.\n" + + "IF YOU'RE DOWNLOADED THIS VERSION FROM THE RELEASES (https://github.com/ByteDream/docker4ssh/releases/latest), MAKE SURE TO CHANGE YOUR SSH KEY IMMEDIATELY BECAUSE ANYONE COULD DECRYPT THE SSH SESSION!!\n" + + "USE 'ssh-keygen -t ed25519 -f /etc/docker4ssh/docker4ssh.key -b 4096' AND UPDATE THE PASSPHRASE IN /etc/docker4ssh/docker4ssh.conf UNDER ssh.Passphrase") + } + + serverConfig, err := ssh.NewSSHConfig(config) + if err != nil { + zap.S().Fatalf("Failed to initialize ssh server config: %v", err) + } + + sshErrChan, sshCloser := ssh.StartServing(config, serverConfig) + zap.S().Infof("Started ssh serving on port %d", config.SSH.Port) + apiErrChan, apiCloser := api.ServeAPI(config) + zap.S().Infof("Started api serving on port %d", config.Api.Port) + + done := make(chan struct{}) + sig := make(chan os.Signal) + signal.Notify(sig, syscall.SIGUSR1, os.Interrupt, os.Kill, syscall.SIGINT, syscall.SIGTERM) + go func() { + s := <-sig + + if sshCloser != nil { + sshCloser() + } + if apiCloser != nil { + apiCloser() + } + + database.GetDatabase().Close() + + if s != syscall.SIGUSR1 { + // Errorf is called here instead of Fatalf because the original exit signal should be kept to exit with it later + zap.S().Errorf("(FATAL actually) received abort signal %d: %s", s.(syscall.Signal), strings.ToUpper(s.String())) + os.Exit(int(s.(syscall.Signal))) + } + + done <- struct{}{} + }() + + select { + case err = <-sshErrChan: + case err = <-apiErrChan: + } + + if err != nil { + zap.S().Errorf("Failed to start working: %v", err) + sig <- os.Interrupt + } else { + select { + case <-sig: + if err != nil { + zap.S().Errorf("Serving failed due error: %v", err) + } else { + zap.S().Info("Serving stopped") + } + default: + sig <- syscall.SIGUSR1 + } + } + + select { + case <-done: + case <-time.After(5 * time.Second): + // if the timeout of 5 seconds expires, forcefully exit + os.Exit(int(syscall.SIGKILL)) + } +} + +func init() { + rootCmd.AddCommand(startCmd) +} diff --git a/server/cmd/validate.go b/server/cmd/validate.go new file mode 100644 index 0000000..16ca617 --- /dev/null +++ b/server/cmd/validate.go @@ -0,0 +1,159 @@ +package cmd + +import ( + c "docker4ssh/config" + "docker4ssh/docker" + "docker4ssh/validate" + "fmt" + "github.com/docker/docker/client" + "github.com/spf13/cobra" + "os" + "path/filepath" + "strings" +) + +var cli *client.Client + +var validateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate docker4ssh specific files (config / profile files)", + + PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) { + cli, err = docker.InitCli() + return err + }, +} + +var validateStrictFlag bool + +var validateConfigCmd = &cobra.Command{ + Use: "config [files]", + Short: "Validate a docker4ssh config file", + + RunE: func(cmd *cobra.Command, args []string) error { + return validateConfig(args) + }, +} + +var validateConfigFileFlag string + +var validateProfileCmd = &cobra.Command{ + Use: "profile [files]", + Short: "Validate docker4ssh profile files", + + RunE: func(cmd *cobra.Command, args []string) error { + return validateProfile(args) + }, +} + +func validateConfig(args []string) error { + config, err := c.LoadConfig(validateConfigFileFlag, false) + if err != nil { + return err + } + + validator := validate.NewConfigValidator(cli, validateStrictFlag, config) + + var result *validate.ValidatorResult + if len(args) == 0 { + result = validator.Validate() + } else { + var validateFuncs []func() *validate.ValidatorResult + for _, arg := range args { + switch strings.ToLower(arg) { + case "profile": + validateFuncs = append(validateFuncs, validator.ValidateProfile) + case "api": + validateFuncs = append(validateFuncs, validator.ValidateAPI) + case "ssh": + validateFuncs = append(validateFuncs, validator.ValidateSSH) + case "database": + validateFuncs = append(validateFuncs, validator.ValidateDatabase) + case "network": + validateFuncs = append(validateFuncs, validator.ValidateNetwork) + case "logging": + validateFuncs = append(validateFuncs, validator.ValidateLogging) + default: + return fmt.Errorf("'%s' is not a valid config section", arg) + } + } + + var errors []*validate.ValidateError + for _, validateFunc := range validateFuncs { + errors = append(errors, validateFunc().Errors...) + } + + result = &validate.ValidatorResult{ + Strict: validateStrictFlag, + Errors: errors, + } + } + + fmt.Println(result.String()) + + if len(result.Errors) > 0 { + os.Exit(1) + } + + return nil +} + +func validateProfile(args []string) error { + var files []string + + if len(args) == 0 { + args = append(args, "/etc/docker4ssh/profile") + } + for _, arg := range args { + stat, err := os.Stat(arg) + if os.IsNotExist(err) { + return fmt.Errorf("file %s does not exist: %v", arg, err) + } + if stat.IsDir() { + dir, err := os.ReadDir(arg) + if err != nil { + return fmt.Errorf("failed to read directory %s: %v", arg, err) + } + for _, file := range dir { + path, err := filepath.Abs(file.Name()) + if err != nil { + return err + } + files = append(files, path) + } + } + } + + var profiles c.Profiles + for _, file := range files { + p, err := c.LoadProfileFile(file, c.HardcodedPreProfile()) + if err != nil { + return err + } + profiles = append(profiles, p...) + } + + var errors []*validate.ValidateError + for _, profile := range profiles { + errors = append(errors, validate.NewProfileValidator(cli, validateStrictFlag, profile).Validate().Errors...) + } + + result := validate.ValidatorResult{ + Strict: validateStrictFlag, + Errors: errors, + } + + fmt.Println(result.String()) + + return nil +} + +func init() { + rootCmd.AddCommand(validateCmd) + validateCmd.PersistentFlags().BoolVarP(&validateStrictFlag, "strict", "s", false, "If the check should be strict") + + validateCmd.AddCommand(validateConfigCmd) + validateConfigCmd.Flags().StringVarP(&validateConfigFileFlag, "file", "f", "/etc/docker4ssh/docker4ssh.conf", "Specify a file to check") + + validateCmd.AddCommand(validateProfileCmd) +} diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..900fc91 --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,189 @@ +package config + +import ( + "fmt" + "github.com/BurntSushi/toml" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" +) + +var globConfig *Config + +type Config struct { + Profile struct { + Dir string `toml:"Dir"` + Default struct { + Password string `toml:"Password"` + NetworkMode int `toml:"NetworkMode"` + Configurable bool `toml:"Configurable"` + RunLevel int `toml:"RunLevel"` + StartupInformation bool `toml:"StartupInformation"` + ExitAfter string `toml:"ExitAfter"` + KeepOnExit bool `toml:"KeepOnExit"` + } `toml:"default"` + Dynamic struct { + Enable bool `toml:"Enable"` + Password string `toml:"Password"` + NetworkMode int `toml:"NetworkMode"` + Configurable bool `toml:"Configurable"` + RunLevel int `toml:"RunLevel"` + StartupInformation bool `toml:"StartupInformation"` + ExitAfter string `toml:"ExitAfter"` + KeepOnExit bool `toml:"KeepOnExit"` + } `toml:"dynamic"` + } `toml:"profile"` + Api struct { + Port uint16 `toml:"Port"` + Configure struct { + Binary string `toml:"Binary"` + Man string `toml:"Man"` + } `toml:"configure"` + } `toml:"api"` + SSH struct { + Port uint16 `toml:"Port"` + Keyfile string `toml:"Keyfile"` + Passphrase string `toml:"Passphrase"` + } `toml:"ssh"` + Database struct { + Sqlite3File string `toml:"Sqlite3File"` + } `toml:"Database"` + Network struct { + Default struct { + Subnet string `toml:"Subnet"` + } `toml:"default"` + Isolate struct { + Subnet string `toml:"Subnet"` + } `toml:"isolate"` + } `toml:"network"` + Logging struct { + Level string `toml:"Level"` + OutputFile string `toml:"OutputFile"` + ErrorFile string `toml:"ErrorFile"` + ConsoleOutput bool `toml:"ConsoleOutput"` + ConsoleError bool `toml:"ConsoleError"` + } `toml:"logging"` +} + +func InitConfig(includeEnv bool) (*Config, error) { + configFiles := []string{ + "./docker4ssh.conf", + "~/.docker4ssh", + "~/.config/docker4ssh.conf", + "/etc/docker4ssh/docker4ssh.conf", + } + + for _, file := range configFiles { + if _, err := os.Stat(file); !os.IsNotExist(err) { + return LoadConfig(file, includeEnv) + } + } + + return nil, fmt.Errorf("no speicfied config file (%s) could be found", strings.Join(configFiles, ", ")) +} + +func LoadConfig(file string, includeEnv bool) (*Config, error) { + config := &Config{} + + if _, err := toml.DecodeFile(file, config); err != nil { + return nil, err + } + + // make paths absolute + dir := filepath.Dir(file) + config.Profile.Dir = absoluteFile(dir, config.Profile.Dir) + config.Api.Configure.Binary = absoluteFile(dir, config.Api.Configure.Binary) + config.Api.Configure.Man = absoluteFile(dir, config.Api.Configure.Man) + config.SSH.Keyfile = absoluteFile(dir, config.SSH.Keyfile) + config.Database.Sqlite3File = absoluteFile(dir, config.Database.Sqlite3File) + config.Logging.OutputFile = absoluteFile(dir, config.Logging.OutputFile) + config.Logging.ErrorFile = absoluteFile(dir, config.Logging.ErrorFile) + + if includeEnv { + if err := updateFromEnv(config); err != nil { + return nil, err + } + } + + return config, nil +} + +func absoluteFile(path, file string) string { + if filepath.IsAbs(file) { + return file + } + return filepath.Join(path, file) +} + +// updateFromEnv looks up if specific environment variable are given which can +// also be used to configure the program. +// Every key in the config file can also be specified via environment variables. +// The env variable syntax is SECTION_KEY -> e.g. DEFAULT_PASSWORD or API_PORT +func updateFromEnv(config *Config) error { + re := reflect.ValueOf(config).Elem() + rt := re.Type() + + for i := 0; i < re.NumField(); i++ { + rf := re.Field(i) + ree := rt.Field(i) + + if err := envParseField(strings.ToUpper(ree.Tag.Get("toml")), rf); err != nil { + return err + } + } + return nil +} + +func envParseField(prefix string, value reflect.Value) error { + for j := 0; j < value.NumField(); j++ { + rtt := value.Type().Field(j) + rff := value.Field(j) + + if rff.Kind() == reflect.Struct { + if err := envParseField(fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml"))), rff); err != nil { + return err + } + continue + } + + envName := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml"))) + val, ok := os.LookupEnv(envName) + if !ok { + continue + } + var expected string + switch rff.Kind() { + case reflect.String: + rff.SetString(val) + continue + case reflect.Bool: + b, err := strconv.ParseBool(val) + if err == nil { + rff.SetBool(b) + continue + } + expected = "true / false (boolean)" + case reflect.Uint16: + ui, err := strconv.ParseUint(val, 10, 16) + if err == nil { + rff.SetUint(ui) + continue + } + expected = "number (uint16)" + default: + return fmt.Errorf("parsed not implemented config type '%s'", rff.Kind()) + } + return fmt.Errorf("failed to parse environment variable '%s': cannot parse value '%s' as %s", envName, val, expected) + } + return nil +} + +func GetConfig() *Config { + return globConfig +} + +func SetConfig(config *Config) { + globConfig = config +} diff --git a/server/config/profile.go b/server/config/profile.go new file mode 100644 index 0000000..d67a9a0 --- /dev/null +++ b/server/config/profile.go @@ -0,0 +1,254 @@ +package config + +import ( + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/json" + "fmt" + "github.com/BurntSushi/toml" + "go.uber.org/zap" + "hash" + "os" + "path/filepath" + "regexp" + "strings" +) + +type Profile struct { + name string + Username *regexp.Regexp + Password *regexp.Regexp + passwordHashAlgo hash.Hash + NetworkMode int + Configurable bool + RunLevel int + StartupInformation bool + ExitAfter string + KeepOnExit bool + Image string + ContainerID string +} + +func (p *Profile) Name() string { + return p.name +} + +func (p *Profile) Match(user string, password []byte) bool { + // username should only be nil if profile was generated from Config.Profile.Dynamic + if p.Username == nil || p.Username.MatchString(user) { + if p.passwordHashAlgo != nil { + password = p.passwordHashAlgo.Sum(password) + } + return p.Password.Match(password) + } + return false +} + +type preProfile struct { + Username string + Password string + NetworkMode int + Configurable bool + RunLevel int + StartupInformation bool + ExitAfter string + KeepOnExit bool + Image string + Container string +} + +func LoadProfileFile(path string, defaultPreProfile preProfile) (Profiles, error) { + var rawProfile map[string]interface{} + if _, err := toml.DecodeFile(path, &rawProfile); err != nil { + return nil, err + } + + profiles, err := parseRawProfile(rawProfile, path, defaultPreProfile) + if err != nil { + return nil, err + } + + return profiles, nil +} + +func LoadProfileDir(path string, defaultPreProfile preProfile) (Profiles, error) { + dir, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + var profiles Profiles + for i, profileConf := range dir { + p, err := LoadProfileFile(filepath.Join(path, profileConf.Name()), defaultPreProfile) + if err != nil { + return nil, err + } + profiles = append(profiles, p...) + zap.S().Debugf("Pre-loaded file %d (%s) with %d profile(s)", i+1, profileConf.Name(), len(p)) + } + + return profiles, nil +} + +func parseRawProfile(rawProfile map[string]interface{}, path string, defaultPreProfile preProfile) (profiles []*Profile, err error) { + var count int + for key, value := range rawProfile { + rawValue, err := json.Marshal(value) + if err != nil { + return nil, err + } + pp := preProfile{ + NetworkMode: 3, + RunLevel: 1, + StartupInformation: true, + } + if err = json.Unmarshal(rawValue, &pp); err != nil { + return nil, fmt.Errorf("failed to parse %s profile conf file %s: %v", key, path, err) + } + + var rawUsername string + if rawUsername = strings.TrimPrefix(pp.Username, "regex:"); rawUsername == pp.Username { + rawUsername = strings.ReplaceAll(rawUsername, "*", ".*") + } + if !strings.HasSuffix(rawUsername, "$") { + rawUsername += "$" + } + username, err := regexp.Compile("(?m)" + rawUsername) + if err != nil { + return nil, fmt.Errorf("failed to parse %s profile username regex for conf file %s: %v", key, path, err) + } + + var rawPassword string + if rawPassword = strings.TrimPrefix(pp.Password, "regex:"); rawUsername == pp.Password { + rawPassword = strings.ReplaceAll(rawPassword, "*", ".*") + } + algo, rawPasswordOrHash := getHash(rawPassword) + if algo == nil && rawPasswordOrHash == "" { + rawPasswordOrHash = ".*" + } + if !strings.HasSuffix(rawPasswordOrHash, "$") { + rawPasswordOrHash += "$" + } + password, err := regexp.Compile("(?m)" + rawPasswordOrHash) + if err != nil { + return nil, fmt.Errorf("failed to parse %s profile password regex for conf file %s: %v", key, path, err) + } + + if (pp.Image == "") == (pp.Container == "") { + return nil, fmt.Errorf("failed to interpret %s profile image / container definition for conf file %s: `Image` or `Container` must be specified, not both nor none of them", key, path) + } + + profiles = append(profiles, &Profile{ + name: key, + Username: username, + Password: password, + passwordHashAlgo: algo, + NetworkMode: pp.NetworkMode, + Configurable: pp.Configurable, + RunLevel: pp.RunLevel, + StartupInformation: pp.StartupInformation, + ExitAfter: pp.ExitAfter, + KeepOnExit: pp.KeepOnExit, + Image: pp.Image, + ContainerID: pp.Container, + }) + count++ + zap.S().Debugf("Pre-loaded profile %s (%d)", key, count) + } + return +} + +type Profiles []*Profile + +func (ps Profiles) GetByName(name string) (*Profile, bool) { + for _, profile := range ps { + if profile.name == name { + return profile, true + } + } + return nil, false +} + +func (ps Profiles) Match(user string, password []byte) (*Profile, bool) { + for _, profile := range ps { + if profile.Match(user, password) { + return profile, true + } + } + return nil, false +} + +func DefaultPreProfileFromConfig(config *Config) preProfile { + defaultProfile := config.Profile.Default + + return preProfile{ + Password: defaultProfile.Password, + NetworkMode: defaultProfile.NetworkMode, + Configurable: defaultProfile.Configurable, + RunLevel: defaultProfile.RunLevel, + StartupInformation: defaultProfile.StartupInformation, + ExitAfter: defaultProfile.ExitAfter, + KeepOnExit: defaultProfile.KeepOnExit, + } +} + +func HardcodedPreProfile() preProfile { + return preProfile{ + NetworkMode: 3, + RunLevel: 1, + StartupInformation: true, + } +} + +func DynamicProfileFromConfig(config *Config, defaultPreProfile preProfile) (Profile, error) { + raw, err := json.Marshal(config.Profile.Dynamic) + if err != nil { + return Profile{}, err + } + json.Unmarshal(raw, &defaultPreProfile) + + algo, rawPasswordOrHash := getHash(defaultPreProfile.Password) + if algo == nil && rawPasswordOrHash == "" { + rawPasswordOrHash = ".*" + } + password, err := regexp.Compile("(?m)" + rawPasswordOrHash) + if err != nil { + return Profile{}, fmt.Errorf("failed to parse password regex: %v ", err) + } + + return Profile{ + name: "", + Username: nil, + Password: password, + passwordHashAlgo: algo, + NetworkMode: defaultPreProfile.NetworkMode, + Configurable: defaultPreProfile.Configurable, + RunLevel: defaultPreProfile.RunLevel, + StartupInformation: defaultPreProfile.StartupInformation, + ExitAfter: defaultPreProfile.ExitAfter, + KeepOnExit: defaultPreProfile.KeepOnExit, + }, nil +} + +func getHash(password string) (algo hash.Hash, raw string) { + split := strings.SplitN(password, ":", 1) + + if len(split) == 1 { + return nil, password + } else { + raw = split[1] + } + + switch split[0] { + case "sha1": + algo = sha1.New() + case "sha256": + algo = sha256.New() + case "sha512": + algo = sha512.New() + default: + algo = nil + } + return +} diff --git a/server/database/auth.go b/server/database/auth.go new file mode 100644 index 0000000..324bd90 --- /dev/null +++ b/server/database/auth.go @@ -0,0 +1,67 @@ +package database + +import ( + "golang.org/x/crypto/bcrypt" +) + +type Auth struct { + User *string `json:"user"` + Password *[]byte `json:"password"` +} + +func NewAuth(user string, password []byte) (Auth, error) { + hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) + if err != nil { + return Auth{}, err + } + return Auth{ + &user, + &hash, + }, nil +} + +func NewUnsafeAuth(user string, password []byte) Auth { + auth, _ := NewAuth(user, password) + return auth +} + +func (db *Database) SetAuth(containerID string, auth Auth) error { + if auth.User != nil { + _, err := db.Exec("INSERT INTO auth (container_id, user) VALUES ($1, $2) ON CONFLICT (container_id) DO UPDATE SET user=$2", containerID, *auth.User) + if err != nil { + return err + } + } + if auth.Password != nil { + _, err := db.Exec("INSERT INTO auth (container_id, password) VALUES ($1, $2) ON CONFLICT (container_id) DO UPDATE SET password=$2", containerID, *auth.Password) + if err != nil { + return err + } + } + return nil +} + +// GetAuthByContainer returns the auth by a container id +func (db *Database) GetAuthByContainer(containerID string) (auth Auth, exists bool) { + if err := db.QueryRow("SELECT user, password FROM auth WHERE container_id=$1", containerID).Scan(&auth.User, &auth.Password); err != nil { + return Auth{}, false + } + return auth, true +} + +func (db *Database) GetContainerByAuth(auth Auth) (containerID string, exists bool) { + // return true if `auth` contains a nil pointer or no auth was found in the database. + // hopefully this is no security issue + if auth.User == nil || auth.Password == nil { + return "", false + } + if err := db.QueryRow("SELECT container_id FROM auth WHERE user=$1 AND password=$2 OR password IS NULL", auth.User, auth.Password).Scan(&containerID); err != nil { + return "", false + } + return containerID, true +} + +func (db *Database) DeleteAuth(containerID string) error { + _, err := db.Exec("DELETE FROM auth WHERE container_id=$1", containerID) + return err +} diff --git a/server/database/database.go b/server/database/database.go new file mode 100644 index 0000000..64faf5a --- /dev/null +++ b/server/database/database.go @@ -0,0 +1,34 @@ +package database + +import ( + "database/sql" + _ "github.com/mattn/go-sqlite3" +) + +var globalDB *Database + +type Database struct { + *sql.DB +} + +func newDatabaseConnection(driverName, dataSource string) (*Database, error) { + database, err := sql.Open(driverName, dataSource) + if err != nil { + return nil, err + } + db := &Database{DB: database} + + return db, nil +} + +func NewSqlite3Connection(databaseFile string) (*Database, error) { + return newDatabaseConnection("sqlite3", databaseFile) +} + +func GetDatabase() *Database { + return globalDB +} + +func SetDatabase(database *Database) { + globalDB = database +} diff --git a/server/database/delete.go b/server/database/delete.go new file mode 100644 index 0000000..50d09d7 --- /dev/null +++ b/server/database/delete.go @@ -0,0 +1,11 @@ +package database + +func (db *Database) Delete(containerID string) error { + if _, err := db.Exec("DELETE FROM auth WHERE container_id=$1", containerID); err != nil { + return err + } + if _, err := db.Exec("DELETE FROM settings WHERE container_id=$1", containerID); err != nil { + return err + } + return nil +} diff --git a/server/database/settings.go b/server/database/settings.go new file mode 100644 index 0000000..c35ca6b --- /dev/null +++ b/server/database/settings.go @@ -0,0 +1,75 @@ +package database + +import ( + "database/sql" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// Settings is the raw version of docker.Config +type Settings struct { + NetworkMode *int `json:"network_mode"` + Configurable *bool `json:"configurable"` + RunLevel *int `json:"run_level"` + StartupInformation *bool `json:"startup_information"` + ExitAfter *string `json:"exit_after"` + KeepOnExit *bool `json:"keep_on_exit"` +} + +func (db *Database) SettingsByContainerID(containerID string) (Settings, error) { + row := db.QueryRow("SELECT network_mode, configurable, run_level, startup_information, exit_after, keep_on_exit FROM settings WHERE container_id LIKE $1", fmt.Sprintf("%s%%", containerID)) + + var settings Settings + + if err := row.Scan(&settings.NetworkMode, &settings.Configurable, &settings.RunLevel, &settings.StartupInformation, &settings.ExitAfter, &settings.KeepOnExit); err != nil { + return Settings{}, err + } + return settings, nil +} + +func (db *Database) SetSettings(containerID string, settings Settings) error { + query := make(map[string]interface{}, 0) + + body, _ := json.Marshal(settings) + json.Unmarshal(body, &query) + + var keys, values []string + for k, v := range query { + if v != nil { + keys = append(keys, k) + switch reflect.ValueOf(v).Kind() { + case reflect.String: + values = append(values, fmt.Sprintf("\"%v\"", v)) + case reflect.Bool: + if v.(bool) { + values = append(values, fmt.Sprintf("%v", 1)) + } else { + values = append(values, fmt.Sprintf("%v", 0)) + } + default: + values = append(values, fmt.Sprintf("%v", v)) + } + } + } + + err := db.QueryRow("SELECT 1 FROM settings WHERE container_id=$1", containerID).Scan() + if err == sql.ErrNoRows { + keys = append(keys, "container_id") + values = append(values, fmt.Sprintf("\"%s\"", containerID)) + + _, err = db.Exec(fmt.Sprintf("INSERT INTO settings (%s) VALUES (%s)", strings.Join(keys, ", "), strings.Join(values, ", "))) + } else if len(keys) > 0 { + var set []string + + for i := 0; i < len(keys); i++ { + set = append(set, fmt.Sprintf("%s=%s", keys[i], values[i])) + } + + _, err = db.Exec(fmt.Sprintf("UPDATE settings SET %s WHERE container_id=$1", strings.Join(set, ", ")), containerID) + } else { + err = nil + } + return err +} diff --git a/server/docker/client.go b/server/docker/client.go new file mode 100644 index 0000000..6732433 --- /dev/null +++ b/server/docker/client.go @@ -0,0 +1,12 @@ +package docker + +import ( + "docker4ssh/database" + "github.com/docker/docker/client" +) + +type Client struct { + Client *client.Client + Database *database.Database + Network Network +} diff --git a/server/docker/container.go b/server/docker/container.go new file mode 100644 index 0000000..0d020fc --- /dev/null +++ b/server/docker/container.go @@ -0,0 +1,637 @@ +package docker + +import ( + "archive/tar" + "bytes" + "context" + c "docker4ssh/config" + "docker4ssh/database" + "docker4ssh/terminal" + "encoding/json" + "fmt" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + "go.uber.org/zap" + "io" + "io/fs" + "net" + "os" + "path/filepath" + "reflect" + "strings" + "time" +) + +func simpleContainerFromID(ctx context.Context, client *Client, config Config, containerID string) (*SimpleContainer, error) { + inspect, err := client.Client.ContainerInspect(ctx, containerID) + if err != nil { + return nil, err + } + + sc := &SimpleContainer{ + config: config, + Image: Image{ + ref: inspect.Image, + }, + ContainerID: containerID[:12], + FullContainerID: containerID, + client: client, + cli: client.Client, + } + + sc.init(ctx) + + return sc, nil +} + +// newSimpleContainer creates a new container. +// Currently, only for internal usage, may be changing in future +func newSimpleContainer(ctx context.Context, client *Client, config Config, image Image, containerName string) (*SimpleContainer, error) { + // create a new container from the given image and activate in- and output + resp, err := client.Client.ContainerCreate(ctx, &container.Config{ + Image: image.Ref(), + AttachStderr: true, + AttachStdin: true, + Tty: true, + AttachStdout: true, + OpenStdin: true, + }, nil, nil, nil, containerName) + if err != nil { + return nil, err + } + + sc := &SimpleContainer{ + config: config, + Image: image, + ContainerID: resp.ID[:12], + FullContainerID: resp.ID, + client: client, + cli: client.Client, + } + + sc.init(ctx) + + return sc, nil +} + +// SimpleContainer is the basic struct to control a docker4ssh container +type SimpleContainer struct { + config Config + Image Image + ContainerID string + FullContainerID string + + started bool + + cancel context.CancelFunc + + client *Client + + // cli is just a shortcut for Client.Client + cli *client.Client + + Network struct { + ID string + IP string + } +} + +func (sc *SimpleContainer) init(ctx context.Context) { + // disconnect from default docker network + sc.cli.NetworkDisconnect(ctx, sc.client.Network[Host], sc.FullContainerID, true) +} + +// Start starts the container +func (sc *SimpleContainer) Start(ctx context.Context) error { + if err := sc.cli.ContainerStart(ctx, sc.FullContainerID, types.ContainerStartOptions{}); err != nil { + return err + } + + if !sc.started { + // initializes all settings. + // as third argument is a pseudo empty used to + // call every function in SimpleContainer.updateConfig. + // for the same reason Config.Configurable and + // Config.KeepOnExit are negated from their value in + // sc.config + if err := sc.updateConfig(ctx, Config{ + Configurable: !sc.config.Configurable, + KeepOnExit: !sc.config.KeepOnExit, + }, sc.config); err != nil { + return err + } + sc.started = true + } + + return nil +} + +// Stop stops the container +func (sc *SimpleContainer) Stop(ctx context.Context) error { + timeout := 0 * time.Second + if err := sc.cli.ContainerStop(ctx, sc.FullContainerID, &timeout); err != nil { + return err + } + + if !sc.config.KeepOnExit { + if err := sc.cli.ContainerRemove(ctx, sc.FullContainerID, types.ContainerRemoveOptions{Force: true}); err != nil { + return err + } + // delete all references to the container in the database + return sc.client.Database.Delete(sc.FullContainerID) + } + return nil +} + +func (sc *SimpleContainer) Running(ctx context.Context) (bool, error) { + resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID) + if err != nil { + return false, err + } + return resp.State != nil && resp.State.Running, nil +} + +// WaitUntilStop waits until the container stops running +func (sc *SimpleContainer) WaitUntilStop(ctx context.Context) error { + statusChan, errChan := sc.cli.ContainerWait(ctx, sc.FullContainerID, container.WaitConditionNotRunning) + select { + case err := <-errChan: + return err + case <-statusChan: + } + return nil +} + +// ExecuteConn executes a command in the container and returns the connection to the output +func (sc *SimpleContainer) ExecuteConn(ctx context.Context, command string, args ...string) (net.Conn, error) { + execID, err := sc.cli.ContainerExecCreate(ctx, sc.FullContainerID, types.ExecConfig{ + AttachStdout: true, + AttachStderr: true, + Cmd: append([]string{command}, args...), + }) + resp, err := sc.cli.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{}) + if err != nil { + return nil, err + } + return resp.Conn, err +} + +// Execute executes a command in the container and returns the response after finished +func (sc *SimpleContainer) Execute(ctx context.Context, command string, args ...string) ([]byte, error) { + buf := bytes.Buffer{} + + conn, err := sc.ExecuteConn(ctx, command, args...) + if err != nil { + return nil, err + } + + io.Copy(&buf, conn) + + return buf.Bytes(), nil +} + +// CopyFrom copies a file from the host system to the client. +// Normal files and directories are accepted +func (sc *SimpleContainer) CopyFrom(ctx context.Context, src, dst string) error { + r, _, err := sc.cli.CopyFromContainer(ctx, sc.FullContainerID, src) + if err != nil { + return err + } + defer r.Close() + + tr := tar.NewReader(r) + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + target := filepath.Join(dst, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if _, err := os.Stat(target); os.IsNotExist(err) { + if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + return err + } + } + case tar.TypeReg: + f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return err + } + + if _, err = io.Copy(f, tr); err != nil { + return err + } + _ = f.Close() + } + } +} + +// CopyTo copies a file from the container to host. +// Normal files and directories are accepted +func (sc *SimpleContainer) CopyTo(ctx context.Context, src, dst string) error { + stat, err := os.Stat(src) + if err != nil { + return err + } + + if stat.IsDir() { + err = filepath.Walk(src, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + file, err := os.Open(path) + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + header.Name = strings.TrimPrefix(strings.TrimPrefix(path, src), "/") + + // write every file to the container. + // it might be better to write the file content to a buffer or + // store the file pointer in a slice and write the buffer / stored + // file pointer to the tar writer when every file was walked + // + // TODO: Test if the two described methods are better than sending every file on it's own + buf := &bytes.Buffer{} + + tw := tar.NewWriter(buf) + if err = tw.WriteHeader(header); err != nil { + return err + } + defer tw.Close() + + io.Copy(tw, file) + + err = sc.cli.CopyToContainer(ctx, sc.FullContainerID, dst, buf, types.CopyToContainerOptions{ + AllowOverwriteDirWithFile: true, + }) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + } else { + file, err := os.Open(src) + if err != nil { + return err + } + info, err := os.Lstat(src) + if err != nil { + return err + } + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + header.Name = filepath.Base(src) + + buf := &bytes.Buffer{} + tw := tar.NewWriter(buf) + if err = tw.WriteHeader(header); err != nil { + return err + } + defer tw.Close() + + _, _ = io.Copy(tw, file) + + err = sc.cli.CopyToContainer(ctx, sc.FullContainerID, dst, buf, types.CopyToContainerOptions{ + AllowOverwriteDirWithFile: true, + }) + if err != nil { + return err + } + } + + return nil +} + +// Config returns the current container config +func (sc *SimpleContainer) Config() Config { + return sc.config +} + +// UpdateConfig updates the container config +func (sc *SimpleContainer) UpdateConfig(ctx context.Context, config Config) error { + oldConfig := sc.config + + if err := sc.updateConfig(ctx, oldConfig, config); err != nil { + return err + } + + var ocm, ncm, sm map[string]interface{} + sm = make(map[string]interface{}, 0) + + ocj, _ := json.Marshal(oldConfig) + ncj, _ := json.Marshal(config) + + json.Unmarshal(ocj, &ocm) + json.Unmarshal(ncj, &ncm) + + srt := reflect.TypeOf(database.Settings{}) + + for k, v := range ocm { + newValue := ncm[k] + if v != newValue && newValue != nil { + field, ok := srt.FieldByName(k) + if !ok { + continue + } + + sm[field.Tag.Get("json")] = newValue + } + } + + // marshal the map into new settings + var settings database.Settings + body, _ := json.Marshal(sm) + json.Unmarshal(body, &settings) + + err := sc.client.Database.SetSettings(sc.FullContainerID, settings) + if err != nil { + return err + } + + if config.KeepOnExit { + if _, ok := sc.client.Database.GetAuthByContainer(sc.FullContainerID); !ok { + if err = sc.client.Database.SetAuth(sc.FullContainerID, database.Auth{ + User: &sc.ContainerID, + }); err != nil { + return err + } + } + } + sc.config = config + + return nil +} + +func (sc *SimpleContainer) updateConfig(ctx context.Context, oldConfig, newConfig Config) error { + if newConfig.NetworkMode != oldConfig.NetworkMode { + if err := sc.setNetworkMode(ctx, oldConfig.NetworkMode, newConfig.NetworkMode, sc.client.Network != nil); err != nil { + return err + } + zap.S().Debugf("Set network mode for %s to %s", sc.ContainerID, newConfig.NetworkMode.Name()) + } + if newConfig.Configurable != oldConfig.Configurable { + if err := sc.setConfigurable(ctx, newConfig.Configurable); err != nil { + return err + } + zap.S().Debugf("Set configurable for %s to %t", sc.ContainerID, newConfig.Configurable) + } + if newConfig.ExitAfter != oldConfig.ExitAfter { + sc.setExitAfterListener(ctx, newConfig.RunLevel, newConfig.ExitAfter) + zap.S().Debugf("Set exit after listener for %s", sc.ContainerID) + } + + sc.config = newConfig + return nil +} + +// setNetworkMode changes the network mode for the container +func (sc *SimpleContainer) setNetworkMode(ctx context.Context, oldMode, newMode NetworkMode, networking bool) error { + var networkID string + + if !networking { + networkID = sc.client.Network[Off] + } else { + networkID = sc.client.Network[newMode] + } + + if networkID != "" { + sc.cli.NetworkDisconnect(ctx, sc.client.Network[oldMode], sc.FullContainerID, true) + // connect container to a network + if err := sc.cli.NetworkConnect(ctx, networkID, sc.FullContainerID, &network.EndpointSettings{}); err != nil { + return err + } + } + + // inspect the container to get its ip address (yes i was too lazy to implement + // a service that generates the ips without docker) + resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID) + if err != nil { + return err + } + // update the internal network information + sc.Network.ID = networkID + sc.Network.IP = resp.NetworkSettings.Networks[newMode.NetworkName()].IPAddress + + return nil +} + +func (sc *SimpleContainer) setConfigurable(ctx context.Context, configurable bool) error { + cconfig := c.GetConfig() + + if configurable { + for srcFile, dstDir := range map[string]string{cconfig.Api.Configure.Binary: "/bin", cconfig.Api.Configure.Man: "/usr/share/man/man1"} { + if err := sc.CopyTo(ctx, srcFile, dstDir); err != nil { + if strings.HasSuffix(dstDir, "/man1") { + // man files aren't that necessary, so if the copy fails it throws only a warning. + // this error gets thrown when the container is alpine linux, for example. + // it does not have a /usr/share/man/man1 directory and the copy fails + // TODO: Create a directory if not existing to prevent this error + zap.S().Warnf("Failed to copy %s to %s/%s for %s: %v", srcFile, dstDir, filepath.Base(srcFile), sc.ContainerID, err) + continue + } else { + return fmt.Errorf("failed to copy %s to %s/%s for %s: %v", srcFile, dstDir, filepath.Base(srcFile), sc.ContainerID, err) + } + } + zap.S().Debugf("Copied %s to %s (%s)", srcFile, filepath.Join(dstDir, filepath.Base(srcFile)), sc.ContainerID) + } + resp, err := sc.cli.ContainerInspect(ctx, sc.FullContainerID) + if err != nil { + return err + } + _, err = sc.Execute(ctx, "sh", "-c", fmt.Sprintf("echo -n %s:%d > /etc/docker4ssh", resp.NetworkSettings.Networks[sc.config.NetworkMode.NetworkName()].Gateway, cconfig.Api.Port)) + if err != nil { + return err + } + zap.S().Debugf("Set ip and port of server for %s", sc.ContainerID) + } else { + _, err := sc.Execute(ctx, "rm", + "-rf", + fmt.Sprintf("/bin/%s", filepath.Base(cconfig.Api.Configure.Binary)), + fmt.Sprintf("/usr/share/man/man1/%s", filepath.Base(cconfig.Api.Configure.Man)), + "/etc/docker4ssh") + if err != nil { + return err + } + zap.S().Debugf("Removed all configurable related files from %s", sc.ContainerID) + } + + return nil +} + +// setAPIRoute sets the IP and port for docker container tools +func (sc *SimpleContainer) setAPIRoute(ctx context.Context, activate bool) error { + var err error + if activate { + var resp types.ContainerJSON + resp, err = sc.cli.ContainerInspect(ctx, sc.FullContainerID) + if err != nil { + return err + } + cconfig := c.GetConfig() + if resp.NetworkSettings != nil { + _, err = sc.Execute(ctx, "sh", "-c", fmt.Sprintf("echo -n %s:%d > /etc/docker4ssh", resp.NetworkSettings.Networks[sc.config.NetworkMode.NetworkName()].Gateway, cconfig.Api.Port)) + } + } else { + _, err = sc.Execute(ctx, "rm", "-rf", "/etc/docker4ssh") + } + return err +} + +// setExitAfterListener listens for exit after processes +func (sc *SimpleContainer) setExitAfterListener(ctx context.Context, runlevel RunLevel, process string) { + if sc.cancel != nil { + sc.cancel() + } + + if process == "" { + return + } + + cancelCtx, cancel := context.WithCancel(ctx) + sc.cancel = cancel + + go func() { + var rawPid []byte + var err error + + // check for the pid of Config.ExitAfter and wait 1 second if it wasn't found + for { + rawPid, err = sc.Execute(cancelCtx, "pidof", "-s", process) + if len(rawPid) > 0 || err != nil { + break + } + time.Sleep(1 * time.Second) + } + + // sometimes garbage bytes are sent as well, they are getting filtered here + var pid []byte + for _, b := range rawPid { + if b > '0' && b < '9' { + pid = append(pid, b) + } + } + + pid = bytes.TrimSuffix(pid, []byte("\n")) + + if _, err = sc.Execute(cancelCtx, "sh", "-c", fmt.Sprintf("tail --pid=%s -f /dev/null", pid)); err != nil && cancelCtx.Err() == nil { + zap.S().Errorf("Could not wait on process %s (%s) for %s", process, pid, sc.ContainerID) + return + } + + if runlevel != Forever { + sc.Stop(context.Background()) + } + }() +} + +func InteractiveContainerFromID(ctx context.Context, client *Client, config Config, containerID string) (*InteractiveContainer, error) { + sc, err := simpleContainerFromID(ctx, client, config, containerID) + if err != nil { + return nil, err + } + return &InteractiveContainer{ + SimpleContainer: sc, + }, nil +} + +func NewInteractiveContainer(ctx context.Context, cli *Client, config Config, image Image, containerName string) (*InteractiveContainer, error) { + sc, err := newSimpleContainer(ctx, cli, config, image, containerName) + if err != nil { + return nil, err + } + return &InteractiveContainer{ + SimpleContainer: sc, + }, nil +} + +type InteractiveContainer struct { + *SimpleContainer + + terminalCount int +} + +// TerminalCount returns the count of active terminals +func (ic *InteractiveContainer) TerminalCount() int { + return ic.terminalCount +} + +// Terminal creates a new interactive terminal session for the container +func (ic *InteractiveContainer) Terminal(ctx context.Context, term *terminal.Terminal) error { + // get the default shell for the root user + rawShell, err := ic.Execute(ctx, "sh", "-c", "getent passwd root | cut -d : -f 7") + if err != nil { + return err + } + + // here we cut out only newlines (which also could've been done via + // bytes.ReplaceAll or strings.ReplaceAll) and redundant bytes + // which sometimes get returned too and which cannot be interpreted + // by the docker engine + shell := bytes.Buffer{} + for _, b := range rawShell { + if b > ' ' { + shell.WriteByte(b) + } + } + + id, err := ic.cli.ContainerExecCreate(ctx, ic.FullContainerID, types.ExecConfig{ + Tty: true, + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Cmd: []string{shell.String()}, + }) + if err != nil { + return err + } + + resp, err := ic.cli.ContainerExecAttach(ctx, id.ID, types.ExecStartCheck{ + Tty: true, + }) + if err != nil { + return err + } + errChan := make(chan error) + + go func() { + // copy every input to the container + if _, err = io.Copy(term, resp.Conn); err != nil { + errChan <- err + } + errChan <- nil + }() + go func() { + // copy every output from the container + if _, err = io.Copy(resp.Conn, term); err != nil { + errChan <- err + } + errChan <- nil + }() + + ic.terminalCount++ + select { + case err = <-errChan: + resp.Conn.Close() + } + ic.terminalCount-- + + return err +} diff --git a/server/docker/docker.go b/server/docker/docker.go new file mode 100644 index 0000000..2dcc66f --- /dev/null +++ b/server/docker/docker.go @@ -0,0 +1,120 @@ +package docker + +import ( + "github.com/docker/docker/client" + "os" +) + +type NetworkMode int + +const ( + Off NetworkMode = iota + 1 + + // Isolate isolates the container from the host and the host's + // network. Therefore, no configurations can be changed from + // within the container + Isolate + + // Host is the default docker networking configuration + Host + + // Docker is the same configuration you get when you start a + // container via the command line + Docker + + // None disables all isolation between the docker container + // and the host, so inside the network the container can act + // as the host. So it has access to the host's network directly + None +) + +func (nm NetworkMode) Name() string { + switch nm { + case Off: + return "Off" + case Isolate: + return "Iso" + case Host: + return "Host" + case Docker: + return "Docker" + case None: + return "None" + } + return "invalid network" +} + +func (nm NetworkMode) NetworkName() string { + switch nm { + case Off: + return "none" + case Isolate: + return "docker4ssh-full" + case Host: + return "bridge" + case Docker: + return "docker4ssh-def" + case None: + return "none" + } + return "" +} + +type RunLevel int + +const ( + User RunLevel = iota + 1 + Container + Forever +) + +func (rl RunLevel) Name() string { + switch rl { + case User: + return "User" + case Container: + return "Container" + case Forever: + return "Forever" + } + return "" +} + +type Config struct { + // NetworkMode describes the level of isolation of the container to the host system. + // Mostly changes the network of the container, see NetworkNames for more details + NetworkMode NetworkMode + + // If Configurable is true, the container can change settings for itself + Configurable bool + + // RunLevel describes the container behavior. + // If the RunLevel is User, the container will exit when the user disconnects. + // If the RunLevel is Container, the container keeps running if the user disconnects + // and ExitAfter is specified and the specified process has not finished yet. + // If the RunLevel is Forever, the container keeps running forever unless ExitAfter + // is specified and the specified process ends. + // + // Note: It also automatically exits if ExitAfter is specified and the specified + // process ends, even if the user is still connected to the container + RunLevel RunLevel + + // StartupInformation defines if information about the container like its (shorthand) + // container id, NetworkMode, RunLevel, etc. should be shown when connecting to it + StartupInformation bool + + // ExitAfter contains a process name after which end the container should stop + ExitAfter string + + // When KeepOnExit is true, the container won't get deleted if it stops working + KeepOnExit bool +} + +func InitCli() (*client.Client, error) { + return client.NewClientWithOpts() +} + +func IsRunning() bool { + _, err := os.Stat("/var/run/docker.sock") + return !os.IsNotExist(err) +} diff --git a/server/docker/image.go b/server/docker/image.go new file mode 100644 index 0000000..706a64d --- /dev/null +++ b/server/docker/image.go @@ -0,0 +1,41 @@ +package docker + +import ( + "context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" + "io" +) + +type Image struct { + ref string +} + +func (i Image) Ref() string { + return i.ref +} + +// NewImage creates a new Image instance +func NewImage(ctx context.Context, cli *client.Client, ref string) (Image, io.ReadCloser, error) { + summary, err := cli.ImageList(ctx, types.ImageListOptions{ + Filters: filters.NewArgs(filters.Arg("reference", ref)), + }) + if err != nil { + return Image{}, nil, err + } + + if len(summary) > 0 { + return Image{ + ref: ref, + }, nil, nil + } else { + out, err := cli.ImagePull(ctx, ref, types.ImagePullOptions{}) + if err != nil { + return Image{}, nil, err + } + return Image{ + ref: ref, + }, out, nil + } +} diff --git a/server/docker/network.go b/server/docker/network.go new file mode 100644 index 0000000..2196546 --- /dev/null +++ b/server/docker/network.go @@ -0,0 +1,86 @@ +package docker + +import ( + "context" + c "docker4ssh/config" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" +) + +type Network map[NetworkMode]string + +// InitNetwork initializes a new docker4ssh network +func InitNetwork(ctx context.Context, cli *client.Client, config *c.Config) (Network, error) { + n := Network{} + + networks, err := cli.NetworkList(ctx, types.NetworkListOptions{}) + if err != nil { + return nil, err + } + for _, dockerNetwork := range networks { + var mode NetworkMode + + switch dockerNetwork.Name { + case "none": + mode = Off + case "docker4ssh-iso": + mode = Isolate + case "bridge": + mode = Host + case "docker4ssh-def": + mode = Docker + case "host": + mode = None + default: + continue + } + + n[mode] = dockerNetwork.ID + } + + if _, ok := n[Isolate]; !ok { + // create a new network which isolates the container from the host, + // but not from the network + resp, err := cli.NetworkCreate(ctx, "docker4ssh-iso", types.NetworkCreate{ + CheckDuplicate: true, + Driver: "bridge", + EnableIPv6: false, + IPAM: &network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{ + { + Subnet: config.Network.Isolate.Subnet, + }, + }, + }, + }) + if err != nil { + return nil, err + } + n[Isolate] = resp.ID + } + + if _, ok := n[Docker]; !ok { + // the standard network for all containers + resp, err := cli.NetworkCreate(ctx, "docker4ssh-def", types.NetworkCreate{ + CheckDuplicate: true, + Driver: "bridge", + EnableIPv6: false, + IPAM: &network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{ + { + Subnet: config.Network.Default.Subnet, + }, + }, + }, + }) + if err != nil { + return nil, err + } + n[Docker] = resp.ID + } + + return n, nil +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..75ab7a4 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,41 @@ +module docker4ssh + +go 1.17 + +require ( + github.com/BurntSushi/toml v0.4.1 + github.com/docker/docker v20.10.11+incompatible + github.com/docker/go-units v0.4.0 + github.com/mattn/go-sqlite3 v1.14.9 + github.com/morikuni/aec v1.0.0 + github.com/spf13/cobra v1.0.0 + go.uber.org/zap v1.19.1 + golang.org/x/crypto v0.0.0-20211202192323-5770296d904e +) + +require ( + github.com/Microsoft/go-winio v0.4.17 // indirect + github.com/containerd/containerd v1.5.8 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect + google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect + google.golang.org/grpc v1.42.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect +) + +replace golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 => github.com/ByteDream/term v0.0.0-20211025115508-891a970291e6 diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..88ff0dc --- /dev/null +++ b/server/go.sum @@ -0,0 +1,1005 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +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= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ByteDream/term v0.0.0-20211025115508-891a970291e6 h1:JbqdarWZpUOv4b43SN+GD0D5AuE7TSAw+VjctpwWcIY= +github.com/ByteDream/term v0.0.0-20211025115508-891a970291e6/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +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/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.8 h1:NmkCC1/QxyZFBny8JogwLpOy2f+VEbO/f6bV2Mqtwuw= +github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/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/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo= +github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +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/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +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-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-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +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/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +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.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +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/konsorten/go-windows-terminal-sequences v1.0.3/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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/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.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +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/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +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.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/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-20181011144130-49bb7cea24b1/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-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-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-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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/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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= +golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190312151545-0bb0c0a6e846/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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +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-20190418145605-e7d98fc518a7/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-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +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.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/server/logging/logging.go b/server/logging/logging.go new file mode 100644 index 0000000..04e2afb --- /dev/null +++ b/server/logging/logging.go @@ -0,0 +1,32 @@ +package logging + +import ( + "fmt" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func InitLogging(level zap.AtomicLevel, outputFiles, errorFiles []string) { + encoderConfig := zap.NewProductionEncoderConfig() + + encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("[2006-01-02 15:04:05] -") + encoderConfig.ConsoleSeparator = " " + encoderConfig.EncodeLevel = func(level zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(fmt.Sprintf("%s:", level.CapitalString())) + } + encoderConfig.EncodeCaller = nil + + config := zap.NewProductionConfig() + config.EncoderConfig = encoderConfig + config.Encoding = "console" + config.Level = level + config.OutputPaths = outputFiles + config.ErrorOutputPaths = errorFiles + + logger, err := config.Build() + if err != nil { + panic(err) + } + + zap.ReplaceGlobals(logger) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..7138413 --- /dev/null +++ b/server/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "docker4ssh/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/server/ssh/config.go b/server/ssh/config.go new file mode 100644 index 0000000..d95f658 --- /dev/null +++ b/server/ssh/config.go @@ -0,0 +1,68 @@ +package ssh + +import ( + c "docker4ssh/config" + "docker4ssh/database" + "fmt" + "golang.org/x/crypto/ssh" + "io/ioutil" +) + +func NewSSHConfig(config *c.Config) (*ssh.ServerConfig, error) { + db := database.GetDatabase() + + sshConfig := &ssh.ServerConfig{ + MaxAuthTries: 3, + PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { + if containerID, exists := db.GetContainerByAuth(database.NewUnsafeAuth(conn.User(), password)); exists && containerID != "" { + return &ssh.Permissions{ + CriticalOptions: map[string]string{ + "containerID": containerID, + }, + }, nil + } else if profile, ok := profiles.Match(conn.User(), password); ok { + return &ssh.Permissions{ + CriticalOptions: map[string]string{ + "profile": profile.Name(), + }, + }, nil + } else if config.Profile.Dynamic.Enable && dynamicProfile.Match(conn.User(), password) { + return &ssh.Permissions{ + CriticalOptions: map[string]string{ + "profile": "dynamic", + "image": conn.User(), + }, + }, nil + } + // i think logging the wrong password is a bit unsafe. + // if you have e.g. just a type in it isn't very well to see your nearly correct password in the logs + return nil, fmt.Errorf("%s tried to connect with user %s but entered wrong a password", conn.RemoteAddr().String(), conn.User()) + }, + } + sshConfig.SetDefaults() + + key, err := parseSSHPrivateKey(config.SSH.Keyfile, []byte(config.SSH.Passphrase)) + if err != nil { + return nil, err + } + sshConfig.AddHostKey(key) + + return sshConfig, nil +} + +func parseSSHPrivateKey(path string, password []byte) (ssh.Signer, error) { + keyBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + var key ssh.Signer + if len(password) == 0 { + key, err = ssh.ParsePrivateKey(keyBytes) + } else { + key, err = ssh.ParsePrivateKeyWithPassphrase(keyBytes, password) + } + if err != nil { + return nil, err + } + return key, nil +} diff --git a/server/ssh/connection.go b/server/ssh/connection.go new file mode 100644 index 0000000..4379cbc --- /dev/null +++ b/server/ssh/connection.go @@ -0,0 +1,201 @@ +package ssh + +import ( + "bytes" + "context" + "database/sql" + "docker4ssh/database" + "docker4ssh/docker" + "docker4ssh/utils" + "fmt" + "go.uber.org/zap" + "strconv" + "sync" + "time" +) + +var ( + allContainers []*docker.InteractiveContainer +) + +func closeAllContainers(ctx context.Context) { + var wg sync.WaitGroup + for _, container := range allContainers { + wg.Add(1) + container := container + go func() { + container.Stop(ctx) + wg.Done() + }() + } + wg.Wait() +} + +func connection(client *docker.Client, user *User) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + container, ok := getContainer(ctx, client, user) + if !ok { + zap.S().Errorf("Failed to create container for %s", user.ID) + return + } + + user.Container = container.SimpleContainer + + var found bool + for _, cont := range allContainers { + if cont == container { + found = true + } + } + if !found { + allContainers = append(allContainers, container) + } + + // check if the container is running and start it if not + if running, err := container.Running(ctx); err == nil && !running { + if err = container.Start(ctx); err != nil { + zap.S().Errorf("Failed to start container %s: %v", container.ContainerID, err) + fmt.Fprintln(user.Terminal, "Failed to start container") + return + } + zap.S().Infof("Started container %s with internal id '%s', ip '%s'", container.ContainerID, container.ContainerID, container.Network.IP) + } else if err != nil { + zap.S().Errorf("Failed to get container running state: %v", err) + fmt.Fprintln(user.Terminal, "Failed to check container running state") + } + + config := container.Config() + if user.Profile.StartupInformation { + buf := &bytes.Buffer{} + fmt.Fprintf(buf, "β”Œβ”€β”€β”€Container────────────────┐\r\n") + fmt.Fprintf(buf, "β”‚ Container ID: %-12s β”‚\r\n", container.ContainerID) + fmt.Fprintf(buf, "β”‚ Network Mode: %-12s β”‚\r\n", config.NetworkMode.Name()) + fmt.Fprintf(buf, "β”‚ Configurable: %-12t β”‚\r\n", config.Configurable) + fmt.Fprintf(buf, "β”‚ Run Level: %-12s β”‚\r\n", config.RunLevel.Name()) + fmt.Fprintf(buf, "β”‚ Exit After: %-12s β”‚\r\n", config.ExitAfter) + fmt.Fprintf(buf, "β”‚ Keep On Exit: %-12t β”‚\r\n", config.KeepOnExit) + fmt.Fprintf(buf, "└──────────────Informationβ”€β”€β”€β”˜\r\n") + user.Terminal.Write(buf.Bytes()) + } + + // start a new terminal session + if err := container.Terminal(ctx, user.Terminal); err != nil { + zap.S().Errorf("Failed to serve %s terminal: %v", container.ContainerID, err) + fmt.Fprintln(user.Terminal, "Failed to serve terminal") + } + + if config.RunLevel == docker.User && container.TerminalCount() == 0 { + if err := container.Stop(ctx); err != nil { + zap.S().Errorf("Error occoured while stopping container %s: %v", container.ContainerID, err) + } else { + lenBefore := len(allContainers) + for i, cont := range allContainers { + if cont == container { + allContainers[i] = allContainers[lenBefore-1] + allContainers = allContainers[:lenBefore-1] + break + } + } + if lenBefore == len(allContainers) { + zap.S().Warnf("Stopped container %s, but failed to remove it from the global container scope", container.ContainerID) + } else { + zap.S().Infof("Stopped container %s", container.ContainerID) + } + } + } + + zap.S().Infof("Stopped session for user %s", user.ID) +} + +func getContainer(ctx context.Context, client *docker.Client, user *User) (container *docker.InteractiveContainer, ok bool) { + db := database.GetDatabase() + var config docker.Config + + // check if the user has a container (id) assigned + if user.Profile.ContainerID != "" { + for _, cont := range allContainers { + if cont.FullContainerID == user.Profile.ContainerID { + return cont, true + } + } + + settings, err := db.SettingsByContainerID(user.Profile.ContainerID) + if err != nil { + zap.S().Errorf("Failed to get stored container config for container %s: %v", user.Profile.ContainerID, err) + fmt.Fprintf(user.Terminal, "Could not connect to saved container") + return nil, false + } + + config = docker.Config{ + NetworkMode: docker.NetworkMode(*settings.NetworkMode), + Configurable: *settings.Configurable, + RunLevel: docker.RunLevel(*settings.RunLevel), + StartupInformation: *settings.StartupInformation, + ExitAfter: *settings.ExitAfter, + KeepOnExit: *settings.KeepOnExit, + } + + container, err = docker.InteractiveContainerFromID(ctx, client, config, user.Profile.ContainerID) + if err != nil { + zap.S().Errorf("Failed to get container from id %s: %v", user.Profile.ContainerID, err) + fmt.Fprintf(user.Terminal, "Failed to get container") + return nil, false + } + + zap.S().Infof("Re-used container %s for user %s", user.Profile.ContainerID, user.ID) + } else { + config = docker.Config{ + NetworkMode: docker.NetworkMode(user.Profile.NetworkMode), + Configurable: user.Profile.Configurable, + RunLevel: docker.RunLevel(user.Profile.RunLevel), + StartupInformation: user.Profile.StartupInformation, + ExitAfter: user.Profile.ExitAfter, + KeepOnExit: user.Profile.KeepOnExit, + } + + image, out, err := docker.NewImage(ctx, client.Client, user.Profile.Image) + if err != nil { + zap.S().Errorf("Failed to get '%s' image for profile %s: %v", user.Profile.Image, user.Profile.Name(), err) + fmt.Fprintf(user.Terminal, "Failed to get image %s", image.Ref()) + return nil, false + } + if out != nil { + if err := utils.DisplayJSONMessagesStream(out, user.Terminal, user.Terminal); err != nil { + zap.S().Fatalf("Failed to fetch '%s' docker image: %v", image.Ref(), err) + fmt.Fprintf(user.Terminal, "Failed to fetch image %s", image.Ref()) + return nil, false + } + } + + container, err = docker.NewInteractiveContainer(ctx, client, config, image, strconv.Itoa(int(time.Now().Unix()))) + if err != nil { + zap.S().Errorf("Failed to create interactive container: %v", err) + fmt.Fprintln(user.Terminal, "Failed to create interactive container") + return nil, false + } + + zap.S().Infof("Created new %s container (%s) for user %s", image.Ref(), container.ContainerID, user.ID) + } + + if _, err := db.SettingsByContainerID(container.FullContainerID); err != nil { + if err == sql.ErrNoRows { + rawNetworkMode := int(config.NetworkMode) + rawRunLevel := int(config.RunLevel) + if err := db.SetSettings(container.FullContainerID, database.Settings{ + NetworkMode: &rawNetworkMode, + Configurable: &config.Configurable, + RunLevel: &rawRunLevel, + StartupInformation: &config.StartupInformation, + ExitAfter: &config.ExitAfter, + KeepOnExit: &config.KeepOnExit, + }); err != nil { + zap.S().Errorf("Failed to update settings for container %s for user %s: %v", container.ContainerID, user.ID, err) + return nil, false + } + } + } + + return container, true +} diff --git a/server/ssh/handle.go b/server/ssh/handle.go new file mode 100644 index 0000000..9c3bf5e --- /dev/null +++ b/server/ssh/handle.go @@ -0,0 +1,79 @@ +package ssh + +import ( + "docker4ssh/docker" + "fmt" + "go.uber.org/zap" + "golang.org/x/crypto/ssh" +) + +type RequestType string + +const ( + RequestPtyReq RequestType = "pty-req" + RequestWindowChange RequestType = "window-change" +) + +type PtyReqPayload struct { + Term string + + Width, Height uint32 + PixelWidth, PixelHeight uint32 + + Modes []byte +} + +func handleChannels(chans <-chan ssh.NewChannel, client *docker.Client, user *User) { + for channel := range chans { + go handleChannel(channel, client, user) + } +} + +func handleChannel(channel ssh.NewChannel, client *docker.Client, user *User) { + if t := channel.ChannelType(); t != "session" { + channel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) + return + } + + conn, requests, err := channel.Accept() + if err != nil { + zap.S().Warnf("Failed to accept channel for user %s", user.ID) + return + } + defer conn.Close() + user.Terminal.ReadWriter = conn + + // handle all other request besides the normal user input. + // currently, only 'pty-req' is implemented which determines a terminal size change + go handleRequest(requests, user) + + // this handles the actual user terminal connection. + // blocks until the session has finished + connection(client, user) + + zap.S().Debugf("Session for user %s ended", user.ID) +} + +func handleRequest(requests <-chan *ssh.Request, user *User) { + for request := range requests { + switch RequestType(request.Type) { + case RequestPtyReq: + // this could spam the logs when the user resizes his window constantly + // log() + + var ptyReq PtyReqPayload + ssh.Unmarshal(request.Payload, &ptyReq) + + user.Terminal.Width = ptyReq.Width + user.Terminal.Height = ptyReq.Height + case RequestWindowChange: + // prevent from logging + default: + zap.S().Debugf("New request from user %s - Type: %s, Want Reply: %t, Payload: '%s'", user.ID, request.Type, request.WantReply, request.Payload) + } + + if request.WantReply { + request.Reply(true, nil) + } + } +} diff --git a/server/ssh/ssh.go b/server/ssh/ssh.go new file mode 100644 index 0000000..fc4c955 --- /dev/null +++ b/server/ssh/ssh.go @@ -0,0 +1,190 @@ +package ssh + +import ( + "context" + "crypto/md5" + c "docker4ssh/config" + "docker4ssh/database" + "docker4ssh/docker" + "docker4ssh/terminal" + "encoding/hex" + "fmt" + "go.uber.org/zap" + "golang.org/x/crypto/ssh" + "net" + "regexp" + "strings" +) + +var ( + users = make([]*User, 0) + + profiles c.Profiles + dynamicProfile c.Profile +) + +type User struct { + *ssh.ServerConn + + ID string + IP string + Profile *c.Profile + Terminal *terminal.Terminal + Container *docker.SimpleContainer +} + +func GetUser(ip string) *User { + for _, user := range users { + if container := user.Container; container != nil && container.Network.IP == ip { + return user + } + } + return nil +} + +type extras struct { + containerID string +} + +func StartServing(config *c.Config, serverConfig *ssh.ServerConfig) (errChan chan error, closer func() error) { + errChan = make(chan error, 1) + + var err error + profiles, err = c.LoadProfileDir(config.Profile.Dir, c.DefaultPreProfileFromConfig(config)) + if err != nil { + errChan <- err + return + } + zap.S().Debugf("Loaded %d profile(s)", len(profiles)) + + if config.Profile.Dynamic.Enable { + dynamicProfile, err = c.DynamicProfileFromConfig(config, c.DefaultPreProfileFromConfig(config)) + if err != nil { + errChan <- err + return + } + zap.S().Debugf("Loaded dynamic profile") + } + + cli, err := docker.InitCli() + if err != nil { + errChan <- err + return + } + zap.S().Debugf("Initialized docker cli") + + network, err := docker.InitNetwork(context.Background(), cli, config) + if err != nil { + errChan <- err + return + } + zap.S().Debugf("Initialized docker networks") + + client := &docker.Client{ + Client: cli, + Database: database.GetDatabase(), + Network: network, + } + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", config.SSH.Port)) + if err != nil { + errChan <- err + return + } + zap.S().Debugf("Created ssh listener") + + var closed bool + go func() { + db := database.GetDatabase() + + for { + conn, err := listener.Accept() + if err != nil { + if closed { + return + } + zap.S().Errorf("Failed to accept new ssh user: %v", err) + continue + } + serverConn, chans, requests, err := ssh.NewServerConn(conn, serverConfig) + if err != nil { + zap.S().Errorf("Failed to establish new ssh connection: %v", err) + continue + } + + idBytes := md5.Sum([]byte(strings.Split(serverConn.User(), ":")[0])) + idString := hex.EncodeToString(idBytes[:]) + + zap.S().Infof("New ssh connection from %s with %s (%s)", serverConn.RemoteAddr().String(), serverConn.ClientVersion(), idString) + + var profile *c.Profile + if name, ok := serverConn.Permissions.CriticalOptions["profile"]; ok { + if name == "dynamic" { + if image, ok := serverConn.Permissions.CriticalOptions["image"]; ok { + tempDynamicProfile := dynamicProfile + tempDynamicProfile.Image = image + profile = &tempDynamicProfile + } + } + if profile == nil { + if profile, ok = profiles.GetByName(name); !ok { + zap.S().Errorf("Failed to get profile %s", name) + continue + } + } + } else if containerID, ok := serverConn.Permissions.CriticalOptions["containerID"]; ok { + if settings, err := db.SettingsByContainerID(containerID); err == nil { + profile = &c.Profile{ + NetworkMode: *settings.NetworkMode, + Configurable: *settings.Configurable, + RunLevel: *settings.RunLevel, + StartupInformation: *settings.StartupInformation, + ExitAfter: *settings.ExitAfter, + KeepOnExit: *settings.KeepOnExit, + ContainerID: containerID, + } + } else { + for _, container := range allContainers { + if container.ContainerID == containerID { + cconfig := c.GetConfig() + profile = &c.Profile{ + Password: regexp.MustCompile(cconfig.Profile.Default.Password), + NetworkMode: cconfig.Profile.Default.NetworkMode, + Configurable: cconfig.Profile.Default.Configurable, + RunLevel: cconfig.Profile.Default.RunLevel, + StartupInformation: cconfig.Profile.Default.StartupInformation, + ExitAfter: cconfig.Profile.Default.ExitAfter, + KeepOnExit: cconfig.Profile.Default.KeepOnExit, + Image: "", + ContainerID: containerID, + } + } + } + } + } + + zap.S().Debugf("User %s has profile %s", idString, profile.Name()) + + user := &User{ + ServerConn: serverConn, + ID: idString, + Terminal: &terminal.Terminal{}, + Profile: profile, + } + users = append(users, user) + + go ssh.DiscardRequests(requests) + go handleChannels(chans, client, user) + } + }() + + return errChan, func() error { + closed = true + + // close all containers + closeAllContainers(context.Background()) + + // close the listener + return listener.Close() + } +} diff --git a/server/terminal/terminal.go b/server/terminal/terminal.go new file mode 100644 index 0000000..c9fc895 --- /dev/null +++ b/server/terminal/terminal.go @@ -0,0 +1,9 @@ +package terminal + +import "io" + +type Terminal struct { + io.ReadWriter + + Width, Height uint32 +} diff --git a/server/utils/convert.go b/server/utils/convert.go new file mode 100644 index 0000000..ad439d4 --- /dev/null +++ b/server/utils/convert.go @@ -0,0 +1,27 @@ +package utils + +import ( + "regexp" + "strings" +) + +func UsernameToRegex(username string) (*regexp.Regexp, error) { + var rawUsername string + if rawUsername = strings.TrimPrefix(username, "regex:"); rawUsername == username { + rawUsername = strings.ReplaceAll(rawUsername, "*", ".*") + } + return regexp.Compile(rawUsername) +} + +func PasswordToRegex(password string) (*regexp.Regexp, error) { + splitPassword := strings.SplitN(password, ":", 1) + if len(splitPassword) > 1 { + switch splitPassword[0] { + case "regex": + return regexp.Compile(splitPassword[1]) + case "sha1", "sha256", "sha512": + password = splitPassword[1] + } + } + return regexp.Compile(strings.ReplaceAll(password, "*", ".*")) +} diff --git a/server/utils/jsonmessage.go b/server/utils/jsonmessage.go new file mode 100644 index 0000000..e8c5d72 --- /dev/null +++ b/server/utils/jsonmessage.go @@ -0,0 +1,238 @@ +// adopted from https://github.com/docker/cli/blob/a32cd16160f1b41c1c4ae7bee4dac929d1484e59/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go + +package utils + +import ( + "docker4ssh/terminal" + "encoding/json" + "fmt" + "github.com/docker/go-units" + "github.com/morikuni/aec" + "io" + "strings" + "time" +) + +// RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to +// ensure the formatted time is always the same number of characters. +const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" + +// JSONError wraps a concrete Code and Message, `Code` is +// an integer error code, `Message` is the error message. +type JSONError struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +func (e *JSONError) Error() string { + return e.Message +} + +// JSONProgress describes a progress. terminalFd is the fd of the current terminal, +// Start is the initial value for the operation. Current is the current status and +// value of the progress made towards Total. Total is the end value describing when +// we made 100% progress for an operation. +type JSONProgress struct { + Terminal *terminal.Terminal + Current int64 `json:"current,omitempty"` + Total int64 `json:"total,omitempty"` + Start int64 `json:"start,omitempty"` + // If true, don't show xB/yB + HideCounts bool `json:"hidecounts,omitempty"` + Units string `json:"units,omitempty"` +} + +func (p *JSONProgress) String() string { + var ( + width = p.Terminal.Width + pbBox string + numbersBox string + timeLeftBox string + ) + if p.Current <= 0 && p.Total <= 0 { + return "" + } + if p.Total <= 0 { + switch p.Units { + case "": + current := units.HumanSize(float64(p.Current)) + return fmt.Sprintf("%8v", current) + default: + return fmt.Sprintf("%d %s", p.Current, p.Units) + } + } + + percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 + if percentage > 50 { + percentage = 50 + } + if width > 110 { + // this number can't be negative gh#7136 + numSpaces := 0 + if 50-percentage > 0 { + numSpaces = 50 - percentage + } + pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) + } + + switch { + case p.HideCounts: + case p.Units == "": // no units, use bytes + current := units.HumanSize(float64(p.Current)) + total := units.HumanSize(float64(p.Total)) + + numbersBox = fmt.Sprintf("%8v/%v", current, total) + + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%8v", current) + } + default: + numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) + + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) + } + } + + if p.Current > 0 && p.Start > 0 && percentage < 50 { + fromStart := time.Now().Sub(time.Unix(p.Start, 0)) + perEntry := fromStart / time.Duration(p.Current) + left := time.Duration(p.Total-p.Current) * perEntry + left = (left / time.Second) * time.Second + + if width > 50 { + timeLeftBox = " " + left.String() + } + } + return pbBox + numbersBox + timeLeftBox +} + +// JSONMessage defines a message struct. It describes +// the created time, where it from, status, ID of the +// message. It's used for docker events. +type JSONMessage struct { + Stream string `json:"stream,omitempty"` + Status string `json:"status,omitempty"` + Progress *JSONProgress `json:"progressDetail,omitempty"` + ProgressMessage string `json:"progress,omitempty"` // deprecated + ID string `json:"id,omitempty"` + From string `json:"from,omitempty"` + Time int64 `json:"time,omitempty"` + TimeNano int64 `json:"timeNano,omitempty"` + Error *JSONError `json:"errorDetail,omitempty"` + ErrorMessage string `json:"error,omitempty"` // deprecated + // Aux contains out-of-band data, such as digests for push signing and image id after building. + Aux *json.RawMessage `json:"aux,omitempty"` +} + +func clearLine(out io.Writer) { + eraseMode := aec.EraseModes.All + cl := aec.EraseLine(eraseMode) + fmt.Fprint(out, cl) +} + +func cursorUp(out io.Writer, l uint) { + fmt.Fprint(out, aec.Up(l)) +} + +func cursorDown(out io.Writer, l uint) { + fmt.Fprint(out, aec.Down(l)) +} + +// Display displays the JSONMessage to `out`. If `isTerminal` is true, it will erase the +// entire current line when displaying the progressbar. +func (jm *JSONMessage) Display(out io.Writer) error { + if jm.Error != nil { + if jm.Error.Code == 401 { + return fmt.Errorf("authentication is required") + } + return jm.Error + } + var endl string + if jm.Stream == "" && jm.Progress != nil { + clearLine(out) + endl = "\r" + fmt.Fprint(out, endl) + } else if jm.Progress != nil && jm.Progress.String() != "" { // disable progressbar in non-terminal + return nil + } + if jm.TimeNano != 0 { + fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed)) + } else if jm.Time != 0 { + fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed)) + } + if jm.ID != "" { + fmt.Fprintf(out, "%s: ", jm.ID) + } + if jm.From != "" { + fmt.Fprintf(out, "(from %s) ", jm.From) + } + if jm.Progress != nil { + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) + } else if jm.ProgressMessage != "" { // deprecated + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) + } else if jm.Stream != "" { + fmt.Fprintf(out, "%s%s", jm.Stream, endl) + } else { + fmt.Fprintf(out, "%s%s\r\n", jm.Status, endl) + } + return nil +} + +// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal` +// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of +// each line and move the cursor while displaying. +func DisplayJSONMessagesStream(in io.Reader, out io.Writer, term *terminal.Terminal) error { + var ( + dec = json.NewDecoder(in) + ids = make(map[string]uint) + ) + + for { + var diff uint + var jm JSONMessage + if err := dec.Decode(&jm); err != nil { + if err == io.EOF { + break + } + return err + } + + if jm.Progress != nil { + jm.Progress.Terminal = term + } + if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { + line, ok := ids[jm.ID] + if !ok { + // NOTE: This approach of using len(id) to + // figure out the number of lines of history + // only works as long as we clear the history + // when we output something that's not + // accounted for in the map, such as a line + // with no ID. + line = uint(len(ids)) + ids[jm.ID] = line + fmt.Fprintf(out, "\r\n") + } + diff = uint(len(ids)) - line + cursorUp(out, diff) + } else { + // When outputting something that isn't progress + // output, clear the history of previous lines. We + // don't want progress entries from some previous + // operation to be updated (for example, pull -a + // with multiple tags). + ids = make(map[string]uint) + } + err := jm.Display(out) + if jm.ID != "" { + cursorDown(out, diff) + } + if err != nil { + return err + } + } + return nil +} diff --git a/server/validate/error.go b/server/validate/error.go new file mode 100644 index 0000000..fd953f0 --- /dev/null +++ b/server/validate/error.go @@ -0,0 +1,33 @@ +package validate + +import "fmt" + +func newValidateError(section, key string, value interface{}, message string, original error) *ValidateError { + return &ValidateError{ + section: section, + key: key, + value: value, + message: message, + originalError: original, + } +} + +type ValidateError struct { + error + + section string + key string + value interface{} + + message string + + originalError error +} + +func (ve *ValidateError) Error() string { + if ve.originalError != nil { + return fmt.Sprintf("failed to validate %s.%s (%v), %s: %v", ve.section, ve.key, ve.value, ve.message, ve.originalError) + } else { + return fmt.Sprintf("failed to validate %s.%s (%v), %s", ve.section, ve.key, ve.value, ve.message) + } +} diff --git a/server/validate/validate.go b/server/validate/validate.go new file mode 100644 index 0000000..890489d --- /dev/null +++ b/server/validate/validate.go @@ -0,0 +1,45 @@ +package validate + +import ( + "fmt" + "github.com/docker/docker/client" + "strings" +) + +type Validator struct { + Cli *client.Client + Strict bool +} + +type ValidatorResult struct { + Strict bool + + Errors []*ValidateError +} + +func (vr *ValidatorResult) Ok() bool { + return len(vr.Errors) == 0 +} + +func (vr *ValidatorResult) String() string { + builder := strings.Builder{} + + if len(vr.Errors) == 0 { + if vr.Strict { + builder.WriteString("Validated all files, no errors were found. You're good to go (strict mode on)") + } else { + builder.WriteString("Validated all files, no errors were found. You're good to go") + } + } else { + if vr.Strict { + builder.WriteString(fmt.Sprintf("Found %d errors (strict mode on)\n\n", len(vr.Errors))) + } else { + builder.WriteString(fmt.Sprintf("Found %d errors\n\n", len(vr.Errors))) + } + for _, err := range vr.Errors { + builder.WriteString(fmt.Sprintf("%v\n", err)) + } + } + + return builder.String() +} diff --git a/server/validate/validate_config.go b/server/validate/validate_config.go new file mode 100644 index 0000000..4252121 --- /dev/null +++ b/server/validate/validate_config.go @@ -0,0 +1,264 @@ +package validate + +import ( + "docker4ssh/config" + "docker4ssh/docker" + "docker4ssh/utils" + "fmt" + "github.com/docker/docker/client" + "go.uber.org/zap" + s "golang.org/x/crypto/ssh" + "io/ioutil" + "net" + "os" + "path/filepath" + "strings" +) + +func NewConfigValidator(cli *client.Client, strict bool, config *config.Config) *ConfigValidator { + return &ConfigValidator{ + Validator: &Validator{ + Cli: cli, + Strict: strict, + }, + Config: config, + } +} + +type ConfigValidator struct { + *Validator + + Config *config.Config +} + +func (cv *ConfigValidator) Validate() *ValidatorResult { + errors := make([]*ValidateError, 0) + + errors = append(errors, cv.ValidateProfile().Errors...) + errors = append(errors, cv.ValidateAPI().Errors...) + errors = append(errors, cv.ValidateSSH().Errors...) + errors = append(errors, cv.ValidateDatabase().Errors...) + errors = append(errors, cv.ValidateNetwork().Errors...) + errors = append(errors, cv.ValidateLogging().Errors...) + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) ValidateProfile() *ValidatorResult { + errors := make([]*ValidateError, 0) + + errors = append(errors, cv.validateProfileDefault()...) + errors = append(errors, cv.validateProfileDynamic()...) + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) validateProfileDefault() []*ValidateError { + profileDefault := cv.Config.Profile.Default + errors := make([]*ValidateError, 0) + + if _, err := utils.PasswordToRegex(profileDefault.Password); err != nil { + errors = append(errors, newValidateError("profile.default", "Password", profileDefault.Password, "not a valid regex string", err)) + } + networkMode := docker.NetworkMode(profileDefault.NetworkMode) + if docker.Off > networkMode || networkMode > docker.None { + errors = append(errors, newValidateError("profile.default", "NetworkMode", profileDefault.NetworkMode, "not a valid network mode", nil)) + } + runLevel := docker.RunLevel(profileDefault.RunLevel) + if docker.User > runLevel || runLevel > docker.Forever { + errors = append(errors, newValidateError("profile.default", "RunLevel", profileDefault.RunLevel, "is not a valid run level", nil)) + } + + return errors +} + +func (cv *ConfigValidator) validateProfileDynamic() []*ValidateError { + profileDynamic := cv.Config.Profile.Dynamic + errors := make([]*ValidateError, 0) + + if !profileDynamic.Enable && !cv.Strict { + return errors + } + + if _, err := utils.PasswordToRegex(profileDynamic.Password); err != nil { + errors = append(errors, newValidateError("profile.dynamic", "Password", profileDynamic.Password, "not a valid regex string", err)) + } + networkMode := docker.NetworkMode(profileDynamic.NetworkMode) + if docker.Off > networkMode || networkMode > docker.None { + errors = append(errors, newValidateError("profile.dynamic", "NetworkMode", profileDynamic.NetworkMode, "not a valid network mode", nil)) + } + runLevel := docker.RunLevel(profileDynamic.RunLevel) + if docker.User > runLevel || runLevel > docker.Forever { + errors = append(errors, newValidateError("profile.dynamic", "RunLevel", profileDynamic.RunLevel, "is not a valid run level", nil)) + } + + return errors +} + +func (cv *ConfigValidator) ValidateAPI() *ValidatorResult { + api := cv.Config.Api + errors := make([]*ValidateError, 0) + + if cv.Strict && !isPortFree(api.Port) { + errors = append(errors, newValidateError("api", "Port", api.Port, "port is already in use", nil)) + } + + errors = append(errors, cv.validateAPIConfigure().Errors...) + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) validateAPIConfigure() *ValidatorResult { + apiConfigure := cv.Config.Api.Configure + errors := make([]*ValidateError, 0) + + for k, v := range map[string]string{"Binary": apiConfigure.Binary, "Man": apiConfigure.Man} { + path := absolutePath("", v) + if msg, err, ok := fileOk(path); !ok { + errors = append(errors, newValidateError("api.configure", k, path, msg, err)) + } + } + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) ValidateSSH() *ValidatorResult { + ssh := cv.Config.SSH + errors := make([]*ValidateError, 0) + + if cv.Strict && !isPortFree(ssh.Port) { + errors = append(errors, newValidateError("api", "Port", ssh.Port, "port is already in use", nil)) + } + + path := absolutePath("", ssh.Keyfile) + if msg, err, ok := fileOk(path); !ok { + errors = append(errors, newValidateError("ssh", "Keyfile", path, msg, err)) + } else { + keyBytes, err := ioutil.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read file %s: %v", path, err)) + } + if ssh.Passphrase == "" { + if _, err = s.ParsePrivateKey(keyBytes); err != nil { + errors = append(errors, newValidateError("ssh", "Passphrase", ssh.Passphrase, "failed to parse ssh keyfile without password", err)) + } + } else { + if _, err = s.ParsePrivateKeyWithPassphrase(keyBytes, []byte(ssh.Passphrase)); err != nil { + errors = append(errors, newValidateError("ssh", "Passphrase", ssh.Passphrase, "failed to parse ssh keyfile with password", err)) + } + } + } + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) ValidateDatabase() *ValidatorResult { + database := cv.Config.Database + errors := make([]*ValidateError, 0) + + path := absolutePath("", database.Sqlite3File) + if msg, err, ok := fileOk(path); !ok { + errors = append(errors, newValidateError("database", "Sqlite3File", path, msg, err)) + } + + // TODO: implement sql database schema + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) ValidateNetwork() *ValidatorResult { + network := cv.Config.Network + errors := make([]*ValidateError, 0) + + if strings.Index(network.Default.Subnet, "/") == -1 { + errors = append(errors, newValidateError("network.default", "Subnet", network.Default.Subnet, "no network mask is given", nil)) + } else if subnet, _, err := net.ParseCIDR(network.Default.Subnet); err != nil { + errors = append(errors, newValidateError("network.default", "Subnet", network.Default.Subnet, "invalid subnet ip", err)) + } else if subnet == nil { + errors = append(errors, newValidateError("network.default", "Subnet", network.Default.Subnet, "invalid subnet ip", nil)) + } + + if strings.Index(network.Isolate.Subnet, "/") == -1 { + errors = append(errors, newValidateError("network.isolate", "Subnet", network.Isolate.Subnet, "no network mask is given", nil)) + } else if subnet, _, err := net.ParseCIDR(network.Isolate.Subnet); err != nil { + errors = append(errors, newValidateError("network.isolate", "Subnet", network.Isolate.Subnet, "invalid subnet ip", err)) + } else if subnet == nil { + errors = append(errors, newValidateError("network.isolate", "Subnet", network.Isolate.Subnet, "invalid subnet ip", nil)) + } + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func (cv *ConfigValidator) ValidateLogging() *ValidatorResult { + logging := cv.Config.Logging + errors := make([]*ValidateError, 0) + + level := zap.NewAtomicLevel() + if err := level.UnmarshalText([]byte(logging.Level)); err != nil { + errors = append(errors, newValidateError("logging", "Level", logging.Level, "invalid level", err)) + } + if cv.Strict { + path := absolutePath("", logging.OutputFile) + if msg, err, ok := fileOk(path); !ok { + errors = append(errors, newValidateError("logging", "OutputFile", logging.OutputFile, msg, err)) + } + path = absolutePath("", logging.ErrorFile) + if msg, err, ok := fileOk(path); !ok { + errors = append(errors, newValidateError("logging", "ErrorFile", logging.ErrorFile, msg, err)) + } + } + + return &ValidatorResult{ + Strict: cv.Strict, + Errors: errors, + } +} + +func isPortFree(port uint16) bool { + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if listener != nil { + listener.Close() + } + return err == nil && port != 0 +} + +func absolutePath(parentPath, filePath string) (path string) { + if filepath.IsAbs(filePath) { + path = filePath + } else { + path = filepath.Join(parentPath, filePath) + } + return +} + +func fileOk(path string) (string, error, bool) { + if info, err := os.Stat(path); os.IsNotExist(err) { + return "file does not exist", err, false + } else if info.IsDir() { + return "file is an directory", nil, false + } else if err != nil { + return "unexpected error", err, false + } + return "", nil, true +} diff --git a/server/validate/validate_profile.go b/server/validate/validate_profile.go new file mode 100644 index 0000000..ee9d2c7 --- /dev/null +++ b/server/validate/validate_profile.go @@ -0,0 +1,64 @@ +package validate + +import ( + "context" + "docker4ssh/config" + "docker4ssh/docker" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" +) + +func NewProfileValidator(cli *client.Client, strict bool, profile *config.Profile) *ProfileValidator { + return &ProfileValidator{ + Validator: &Validator{ + Cli: cli, + Strict: strict, + }, + Profile: profile, + } +} + +type ProfileValidator struct { + *Validator + + Profile *config.Profile +} + +func (pv *ProfileValidator) Validate() *ValidatorResult { + profile := pv.Profile + errors := make([]*ValidateError, 0) + + networkMode := docker.NetworkMode(profile.NetworkMode) + if docker.Off > networkMode || networkMode > docker.None { + errors = append(errors, newValidateError(profile.Name(), "NetworkMode", profile.NetworkMode, "not a valid network mode", nil)) + } + runLevel := docker.RunLevel(profile.RunLevel) + if docker.User > runLevel || runLevel > docker.Forever { + errors = append(errors, newValidateError(profile.Name(), "RunLevel", profile.RunLevel, "is not a valid run level", nil)) + } + if profile.Image == "" && profile.ContainerID == "" { + errors = append(errors, newValidateError(profile.Name(), "image/container", "", "Image OR Container must be specified, neither both nor none", nil)) + } else if pv.Strict { + if profile.Image != "" { + list, err := pv.Cli.ImageList(context.Background(), types.ImageListOptions{ + Filters: filters.NewArgs(filters.Arg("reference", profile.Image)), + }) + if err != nil || len(list) == 0 { + errors = append(errors, newValidateError(profile.Name(), "Image", profile.Image, "image does not exist", nil)) + } + } else if profile.ContainerID != "" { + list, err := pv.Cli.ContainerList(context.Background(), types.ContainerListOptions{ + Filters: filters.NewArgs(filters.Arg("id", profile.ContainerID)), + }) + if err != nil || len(list) == 0 { + errors = append(errors, newValidateError(profile.Name(), "Image", profile.Image, "container does not exist", nil)) + } + } + } + + return &ValidatorResult{ + Strict: pv.Strict, + Errors: errors, + } +}