From 831ea9889fe0e831bc559f78650b2d596a94fa25 Mon Sep 17 00:00:00 2001 From: liuzhihang1 Date: Wed, 26 Jun 2024 20:45:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + LICENSE | 661 ++++++++++++++++++++++++++++++++ api/api.go | 33 ++ api/config.go | 94 +++++ api/file.go | 34 ++ api/log.go | 20 + api/permission.go | 27 ++ api/proc.go | 101 +++++ api/push.go | 50 +++ api/user.go | 86 +++++ api/ws.go | 96 +++++ boot/boot.go | 52 +++ build.bat | 4 + config.yaml | 8 + config/config.go | 27 ++ consts/consts.go | 1 + consts/ctxflag/ctxflag.go | 7 + consts/permission/permission.go | 9 + consts/role/role.go | 10 + dao/config.go | 30 ++ dao/db.go | 40 ++ dao/permission.go | 42 ++ dao/process.go | 58 +++ dao/push.go | 33 ++ dao/user.go | 51 +++ go.mod | 83 ++++ go.sum | 212 ++++++++++ log/log.go | 44 +++ main.go | 15 + model/config.go | 18 + model/es.go | 103 +++++ model/file.go | 6 + model/message.go | 8 + model/permission.go | 26 ++ model/proc.go | 22 ++ model/process.go | 16 + model/push_msg.go | 14 + model/user.go | 15 + route/middle/panic.go | 26 ++ route/middle/permission.go | 44 +++ route/middle/token.go | 73 ++++ route/route.go | 111 ++++++ server_config.json | 128 +++++++ service/es/es.go | 155 ++++++++ service/file/file.go | 76 ++++ service/log/loghandler.go | 40 ++ service/process/proccess.go | 324 ++++++++++++++++ service/process/process_pty.go | 169 ++++++++ service/process/process_std.go | 182 +++++++++ service/process/service.go | 124 ++++++ service/push/push.go | 35 ++ termui/tui.go | 58 +++ test/test.go | 111 ++++++ utils/jwt.go | 74 ++++ utils/md5.go | 12 + utils/unicode.go | 32 ++ utils/utils.go | 8 + 57 files changed, 3945 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 api/api.go create mode 100644 api/config.go create mode 100644 api/file.go create mode 100644 api/log.go create mode 100644 api/permission.go create mode 100644 api/proc.go create mode 100644 api/push.go create mode 100644 api/user.go create mode 100644 api/ws.go create mode 100644 boot/boot.go create mode 100644 build.bat create mode 100644 config.yaml create mode 100644 config/config.go create mode 100644 consts/consts.go create mode 100644 consts/ctxflag/ctxflag.go create mode 100644 consts/permission/permission.go create mode 100644 consts/role/role.go create mode 100644 dao/config.go create mode 100644 dao/db.go create mode 100644 dao/permission.go create mode 100644 dao/process.go create mode 100644 dao/push.go create mode 100644 dao/user.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log/log.go create mode 100644 main.go create mode 100644 model/config.go create mode 100644 model/es.go create mode 100644 model/file.go create mode 100644 model/message.go create mode 100644 model/permission.go create mode 100644 model/proc.go create mode 100644 model/process.go create mode 100644 model/push_msg.go create mode 100644 model/user.go create mode 100644 route/middle/panic.go create mode 100644 route/middle/permission.go create mode 100644 route/middle/token.go create mode 100644 route/route.go create mode 100644 server_config.json create mode 100644 service/es/es.go create mode 100644 service/file/file.go create mode 100644 service/log/loghandler.go create mode 100644 service/process/proccess.go create mode 100644 service/process/process_pty.go create mode 100644 service/process/process_std.go create mode 100644 service/process/service.go create mode 100644 service/push/push.go create mode 100644 termui/tui.go create mode 100644 test/test.go create mode 100644 utils/jwt.go create mode 100644 utils/md5.go create mode 100644 utils/unicode.go create mode 100644 utils/utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bdd2d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +info.log +msm* +templates +vue-minecraft/node_modules +data.db +route/template +.data* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dbbe355 --- /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/api/api.go b/api/api.go new file mode 100644 index 0000000..96bb8a6 --- /dev/null +++ b/api/api.go @@ -0,0 +1,33 @@ +package api + +import ( + "errors" + "msm/consts/ctxflag" + "net/http" + + "github.com/gin-gonic/gin" +) + +func rOk(ctx *gin.Context, message string, data any) { + jsonData := map[string]any{ + "code": 0, + "msg": message, + } + if data != nil { + jsonData["data"] = data + } + ctx.JSON(http.StatusOK, jsonData) +} + +func errCheck(ctx *gin.Context, isErr bool, errData any) { + if !isErr { + return + } + if err, ok := errData.(error); ok { + ctx.Set(ctxflag.ERR, err) + } + if err, ok := errData.(string); ok { + ctx.Set(ctxflag.ERR, errors.New(err)) + } + panic(0) +} diff --git a/api/config.go b/api/config.go new file mode 100644 index 0000000..cdd4470 --- /dev/null +++ b/api/config.go @@ -0,0 +1,94 @@ +package api + +import ( + "msm/config" + "msm/dao" + "msm/model" + "msm/service/es" + "reflect" + "strconv" + + "github.com/gin-gonic/gin" +) + +type configApi struct{} + +var ConfigApi = new(configApi) + +func (c *configApi) GetSystemConfiguration(ctx *gin.Context) { + typeElem := reflect.TypeOf(config.CF).Elem() + valueElem := reflect.ValueOf(config.CF).Elem() + result := []model.SystemConfigurationResp{} + for i := 0; i < typeElem.NumField(); i++ { + typeField := typeElem.Field(i) + valueField := valueElem.Field(i) + var value any + switch typeField.Type.Kind() { + case reflect.Int64, reflect.Int: + value = valueField.Int() + case reflect.String: + value = valueField.String() + case reflect.Bool: + value = valueField.Bool() + case reflect.Float64: + value = valueField.Float() + default: + continue + } + result = append(result, model.SystemConfigurationResp{ + Key: typeField.Name, + Value: value, + Default: typeField.Tag.Get("default"), + Describe: typeField.Tag.Get("describe"), + }) + } + rOk(ctx, "获取系统配置成功", result) +} + +func (c *configApi) SetSystemConfiguration(ctx *gin.Context) { + data := map[string]string{} + errCheck(ctx, ctx.ShouldBindJSON(&data) != nil, "请求参数错误") + typeElem := reflect.TypeOf(config.CF).Elem() + valueElem := reflect.ValueOf(config.CF).Elem() + for i := 0; i < typeElem.NumField(); i++ { + typeField := typeElem.Field(i) + valueField := valueElem.Field(i) + for k, v := range data { + if typeField.Name == k { + var err error + switch typeField.Type.Kind() { + case reflect.String: + valueField.SetString(v) + case reflect.Bool: + value, errV := strconv.ParseBool(v) + err = errV + if err == nil { + valueField.SetBool(value) + } + case reflect.Float64: + value, errV := strconv.ParseFloat(v, 64) + err = errV + if err == nil { + valueField.SetFloat(value) + } + case reflect.Int64, reflect.Int: + value, errV := strconv.ParseInt(v, 10, 64) + err = errV + if err == nil { + valueField.SetInt(value) + } + default: + continue + } + errCheck(ctx, err != nil, k+"类似错误") + errCheck(ctx, dao.ConfigDao.SetConfigValue(k, v) != nil, "修改配置失败") + } + } + } + rOk(ctx, "修改配置成功", nil) +} + +func (c *configApi) EsConfigReload(ctx *gin.Context) { + errCheck(ctx, !es.InitEs(), "es连接失败,请检查是否启用es或账号密码是否存在错误") + rOk(ctx, "已连接上es", nil) +} diff --git a/api/file.go b/api/file.go new file mode 100644 index 0000000..51f604a --- /dev/null +++ b/api/file.go @@ -0,0 +1,34 @@ +package api + +import ( + FileService "msm/service/file" + + "github.com/gin-gonic/gin" +) + +type file struct{} + +var FileApi = new(file) + +func (f *file) FilePathHandler(ctx *gin.Context) { + data, err := FileService.FileService.GetFileAndDirByPath(ctx.Query("path")) + errCheck(ctx, err != nil, "文件路径查询失败") + rOk(ctx, "文件路径查询成功", data) +} + +func (f *file) FileWriteHandler(ctx *gin.Context) { + path := ctx.PostForm("filePath") + fi, err := ctx.FormFile("data") + errCheck(ctx, err != nil, "文件读取失败") + fiReader, _ := fi.Open() + err = FileService.FileService.UpdateFileData(path, fiReader, fi.Size) + errCheck(ctx, err != nil, "文件数据更新失败") + rOk(ctx, "文件更新成功", nil) +} + +func (f *file) FileReadHandler(ctx *gin.Context) { + path := ctx.Query("filePath") + bytes, err := FileService.FileService.ReadFileFromPath(path) + errCheck(ctx, err != nil, "文件数据读取失败") + rOk(ctx, "文件数据读取成功", string(bytes)) +} diff --git a/api/log.go b/api/log.go new file mode 100644 index 0000000..3fd8ec5 --- /dev/null +++ b/api/log.go @@ -0,0 +1,20 @@ +package api + +import ( + "msm/config" + "msm/model" + "msm/service/es" + + "github.com/gin-gonic/gin" +) + +type logApi struct{} + +var LogApi = new(logApi) + +func (a *logApi) GetLog(ctx *gin.Context) { + req := model.GetLogReq{} + errCheck(ctx, !config.CF.EsEnable, "elasticsearch未启用或账号密码错误") + errCheck(ctx, ctx.ShouldBindJSON(&req) != nil, "请求体格式错误") + rOk(ctx, "查询成功", es.EsService.Search(req)) +} diff --git a/api/permission.go b/api/permission.go new file mode 100644 index 0000000..af4bcf7 --- /dev/null +++ b/api/permission.go @@ -0,0 +1,27 @@ +package api + +import ( + "msm/model" + + "msm/dao" + + "github.com/gin-gonic/gin" +) + +var PermissionApi = new(permissionApi) + +type permissionApi struct{} + +func (p *permissionApi) EditPermssion(ctx *gin.Context) { + per := model.Permission{} + err := ctx.ShouldBindJSON(&per) + errCheck(ctx, err != nil, err) + err = dao.PermissionDao.EditPermssion(per) + errCheck(ctx, err != nil, err) + rOk(ctx, "权限修改成功", nil) +} + +func (p *permissionApi) GetPermissionList(ctx *gin.Context) { + result := dao.PermissionDao.GetPermssionList(ctx.Query("account")) + rOk(ctx, "查询成功", result) +} diff --git a/api/proc.go b/api/proc.go new file mode 100644 index 0000000..857801e --- /dev/null +++ b/api/proc.go @@ -0,0 +1,101 @@ +package api + +import ( + "msm/consts/ctxflag" + "msm/consts/role" + "msm/dao" + "msm/model" + "msm/service/process" + "strconv" + + "github.com/gin-gonic/gin" +) + +type procApi struct{} + +var ProcApi = new(procApi) + +func (p *procApi) CreateNewProcess(ctx *gin.Context) { + req := model.Process{} + ctx.ShouldBindJSON(&req) + index, err := dao.ProcessDao.AddProcessConfig(req) + errCheck(ctx, err != nil, err) + req.Uuid = index + proc, err := process.RunNewProcess(req) + errCheck(ctx, err != nil, err) + process.ProcessCtlService.AddProcess(req.Uuid, proc) + rOk(ctx, "创建成功", gin.H{ + "id": req.Uuid, + }) +} + +func (p *procApi) DeleteNewProcess(ctx *gin.Context) { + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + process.ProcessCtlService.KillProcess(uuid) + process.ProcessCtlService.DeleteProcess(uuid) + err = dao.ProcessDao.DeleteProcessConfig(uuid) + errCheck(ctx, err != nil, err) + rOk(ctx, "删除成功", nil) +} + +func (p *procApi) KillProcess(ctx *gin.Context) { + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + err = process.ProcessCtlService.KillProcess(uuid) + errCheck(ctx, err != nil, err) + rOk(ctx, "成功", nil) +} + +func (p *procApi) StartProcess(ctx *gin.Context) { + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + prod, err := process.ProcessCtlService.GetProcess(uuid) + if err != nil { // 进程不存在则创建 + proc, err := process.RunNewProcess(dao.ProcessDao.GetProcessConfigById(uuid)) + errCheck(ctx, err != nil, err) + process.ProcessCtlService.AddProcess(uuid, proc) + rOk(ctx, "成功", nil) + return + } + errCheck(ctx, prod.GetStateState() == 1, "进程还在运行中") + prod.ResetRestartTimes() + prod.ReStart() + // dao.UpdateServerAutoStart(uuid, true) + rOk(ctx, "成功", nil) +} + +func (p *procApi) GetProcessList(ctx *gin.Context) { + if ctx.GetInt(ctxflag.ROLE) < int(role.USER) { + rOk(ctx, "进程列表获取成功", process.ProcessCtlService.GetProcessList()) + } else { + rOk(ctx, "进程列表获取成功", process.ProcessCtlService.GetProcessListByUser(ctx.GetString(ctxflag.USER_NAME))) + } +} + +func (p *procApi) UpdateProcessConfig(ctx *gin.Context) { + req := model.Process{} + ctx.ShouldBindJSON(&req) + process.ProcessCtlService.UpdateProcessConfig(req) + err := dao.ProcessDao.UpdateProcessConfig(req) + errCheck(ctx, err != nil, err) + rOk(ctx, "更改配置成功", nil) +} + +func (p *procApi) GetProcessConfig(ctx *gin.Context) { + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + data := dao.ProcessDao.GetProcessConfigById(uuid) + errCheck(ctx, data.Uuid == 0, "未查询到信息") + rOk(ctx, "success", data) +} + +func (p *procApi) ProcessControl(ctx *gin.Context) { + user := ctx.GetString(ctxflag.USER_NAME) + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + proc, err := process.ProcessCtlService.GetProcess(uuid) + errCheck(ctx, err != nil, "进程控制权获取失败") + proc.ProcessControl(user) + rOk(ctx, "获取进程控权成功", nil) +} diff --git a/api/push.go b/api/push.go new file mode 100644 index 0000000..595aff9 --- /dev/null +++ b/api/push.go @@ -0,0 +1,50 @@ +package api + +import ( + "msm/model" + "strconv" + + "msm/dao" + + "github.com/gin-gonic/gin" +) + +type pushApi struct{} + +var PushApi = new(pushApi) + +func (p *pushApi) GetPushList(ctx *gin.Context) { + rOk(ctx, "查询成功", dao.PushDao.GetPushList()) +} + +func (p *pushApi) GetPushById(ctx *gin.Context) { + id, err := strconv.Atoi(ctx.Query("id")) + errCheck(ctx, err != nil, err) + rOk(ctx, "查询成功", dao.PushDao.GetPushConfigById(id)) +} + +func (p *pushApi) AddPushConfig(ctx *gin.Context) { + data := model.Push{} + err := ctx.ShouldBindJSON(&data) + errCheck(ctx, err != nil, err) + err = dao.PushDao.AddPushConfig(data) + errCheck(ctx, err != nil, err) + rOk(ctx, "添加成功", nil) +} + +func (p *pushApi) UpdatePushConfig(ctx *gin.Context) { + data := model.Push{} + err := ctx.ShouldBindJSON(&data) + errCheck(ctx, err != nil, err) + err = dao.PushDao.UpdatePushConfig(data) + errCheck(ctx, err != nil, err) + rOk(ctx, "更新成功", nil) +} + +func (p *pushApi) DeletePushConfig(ctx *gin.Context) { + id, err := strconv.Atoi(ctx.Query("id")) + errCheck(ctx, err != nil, err) + err = dao.PushDao.DeletePushConfig(id) + errCheck(ctx, err != nil, err) + rOk(ctx, "删除成功", nil) +} diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..44e4dfd --- /dev/null +++ b/api/user.go @@ -0,0 +1,86 @@ +package api + +import ( + "msm/consts/ctxflag" + "msm/consts/role" + "msm/dao" + "msm/model" + "msm/utils" + + "github.com/gin-gonic/gin" +) + +type userApi struct{} + +var UserApi = new(userApi) + +const DEFAULT_ROOT_PASSWORD = "root" + +func (u *userApi) LoginHandler(ctx *gin.Context) { + info := map[string]string{} + ctx.ShouldBindJSON(&info) + account := info["account"] + password := info["password"] + errCheck(ctx, !u.checkLoginInfo(account, password), "登入失败,账号或密码错误") + token, err := utils.GenToken(account) + errCheck(ctx, err != nil, err) + ctx.JSON(200, gin.H{ + "code": 0, + "msg": "登入成功!", + "token": token, + "username": account, + "role": dao.UserDao.GetUserByName(account).Role, + }) +} + +func (u *userApi) CreateUser(ctx *gin.Context) { + user := model.User{} + err := ctx.ShouldBindJSON(&user) + errCheck(ctx, err != nil, err) + errCheck(ctx, user.Role == int(role.ROOT), "不能添加root账号") + err = dao.UserDao.CreateUser(user) + errCheck(ctx, err != nil, err) + rOk(ctx, "注册成功", nil) +} + +func (u *userApi) ChangePassword(ctx *gin.Context) { + user := model.User{} + err := ctx.ShouldBindJSON(&user) + errCheck(ctx, err != nil, err) + reqUser := ctx.GetString(ctxflag.USER_NAME) + errCheck(ctx, ctx.GetInt(ctxflag.ROLE) != int(role.ROOT) && user.Account != "", "参数错误") + var userName string + if user.Account != "" { + userName = user.Account + } else { + userName = reqUser + } + err = dao.UserDao.UpdatePassword(userName, user.Password) + errCheck(ctx, err != nil, err) + rOk(ctx, "修改密码成功", nil) + +} + +func (u *userApi) DeleteUser(ctx *gin.Context) { + errCheck(ctx, ctx.Query("account") == "root", "无法删除root账户") + err := dao.UserDao.DeleteUser(ctx.Query("account")) + errCheck(ctx, err != nil, "无法删除root账户") + rOk(ctx, "删除成功", nil) +} + +func (u *userApi) GetUserList(ctx *gin.Context) { + rOk(ctx, "查询成功", dao.UserDao.GetUserList()) +} + +func (u *userApi) checkLoginInfo(account, password string) bool { + user := dao.UserDao.GetUserByName(account) + if account == "root" && user.Account == "" { + dao.UserDao.CreateUser(model.User{ + Account: "root", + Password: DEFAULT_ROOT_PASSWORD, + Role: int(role.ROOT), + }) + return password == DEFAULT_ROOT_PASSWORD + } + return user.Password == utils.Md5(password) +} diff --git a/api/ws.go b/api/ws.go new file mode 100644 index 0000000..6416ae3 --- /dev/null +++ b/api/ws.go @@ -0,0 +1,96 @@ +package api + +import ( + "context" + "msm/consts/ctxflag" + "msm/log" + "msm/service/process" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" +) + +type wsApi struct{} + +var WsApi = new(wsApi) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (w *wsApi) WebsocketHandle(ctx *gin.Context) { + reqUser := ctx.GetString(ctxflag.USER_NAME) + uuid, err := strconv.Atoi(ctx.Query("uuid")) + errCheck(ctx, err != nil, "参数有误") + proc, err := process.ProcessCtlService.GetProcess(uuid) + errCheck(ctx, err != nil, "进程获取失败") + errCheck(ctx, proc.GetStateState() != 1, "进程未运行") + errCheck(ctx, proc.GetControlController() != reqUser && !proc.VerifyControl(), "进程权限不足") + errCheck(ctx, !proc.TryLock(), "进程已被占用") + proc.SetWhoUsing(reqUser) + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + errCheck(ctx, err != nil, "ws升级失败") + log.Logger.Infow("ws连接成功", "进程名称", proc.GetName(), "连接者", proc.GetWhoUsing()) + proc.SetControlController("") + wsCtx, cancel := context.WithCancel(context.Background()) + w.startWsConnect(conn, proc, cancel) + proc.SetWsConn(conn) + proc.SetIsUsing(true) + close := func(err string) { + proc.SetWhoUsing("") + proc.SetIsUsing(false) + proc.SetWsConn(nil) + conn.Close() + proc.Unlock() + log.Logger.Infow("ws连接断开", "操作类型", err, "进程名称", proc.GetName()) + } + conn.SetCloseHandler(func(_ int, _ string) error { + proc.ChangControlChan() <- 1 + close("ws连接被断开") + return nil + }) + select { + case signal := <-proc.ChangControlChan(): + { + if signal == 0 { + close("强制断开ws连接") + } + } + case <-proc.StopChan(): + { + close("进程已停止,强制断开ws连接") + } + case <-time.After(time.Minute * 10): + { + close("连接时间超过最大时长限制") + } + case <-wsCtx.Done(): + { + close("tcp连接建立已被关闭") + } + } + +} + +func (w *wsApi) startWsConnect(conn *websocket.Conn, proc process.Process, cancel context.CancelFunc) { + proc.ReadCache(conn) + log.Logger.Debugw("ws读取线程已启动") + go func() { + for { + _, b, err := conn.ReadMessage() + if err != nil { + log.Logger.Debugw("ws读取线程已退出", "info", err) + cancel() + return + } + proc.WriteBytes(b) + } + }() +} diff --git a/boot/boot.go b/boot/boot.go new file mode 100644 index 0000000..fec4354 --- /dev/null +++ b/boot/boot.go @@ -0,0 +1,52 @@ +package boot + +import ( + "msm/config" + "msm/dao" + "msm/log" + "msm/service/es" + "msm/service/process" + "msm/utils" + "reflect" + "strconv" +) + +func Boot() { + initConfiguration() + initEs() + initProcess() +} + +func initConfiguration() { + typeElem := reflect.TypeOf(config.CF).Elem() + valueElem := reflect.ValueOf(config.CF).Elem() + for i := 0; i < typeElem.NumField(); i++ { + typeField := typeElem.Field(i) + valueField := valueElem.Field(i) + value, err := dao.ConfigDao.GetConfigValue(typeField.Name) + if err != nil { + value = typeField.Tag.Get("default") + } + switch typeField.Type.Kind() { + case reflect.String: + valueField.SetString(value) + case reflect.Bool: + valueField.SetBool(utils.Unwarp(strconv.ParseBool(value))) + case reflect.Float64: + valueField.SetFloat(utils.Unwarp(strconv.ParseFloat(value, 64))) + case reflect.Int64, reflect.Int: + valueField.SetInt(utils.Unwarp(strconv.ParseInt(value, 10, 64))) + default: + continue + } + } + log.Logger.Debugw("获取配置信息完成", "Configuration", config.CF) +} + +func initEs() { + es.InitEs() +} + +func initProcess() { + process.ProcessCtlService.ProcessInit() +} diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..ede841d --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ + SET CGO_ENABLED=0 + SET GOOS=linux + SET GOARCH=amd64 + go build -o msm main.go \ No newline at end of file diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..96529ba --- /dev/null +++ b/config.yaml @@ -0,0 +1,8 @@ +LogLevel: "debug" +Listen: ":8797" +EsConfig: + Enable: true + Url: "http://xcon.top:9200" + Index: "server_log_v1" + Username: "elastic" + Password: "1625167628@xcon" \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..da9d5c2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,27 @@ +package config + +var CF = &configuration{ + LogLevel: "debug", +} + +// 只支持 float64、int、int64、bool、string类型 +type configuration struct { + LogLevel string `default:"debug" describe:"日志等级"` + Listen string `default:":8797" describe:"监听端口"` + EsEnable bool `default:"false" describe:"启用Elasticsearch"` + EsUrl string `default:"" describe:"Elasticsearch url"` + EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"` + EsUsername string `default:"" describe:"Elasticsearch用户名"` + EsPassword string `default:"" describe:"Elasticsearch密码"` + FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"` + ProcessInputPrefix string `default:">" describe:"进程输入前缀"` + ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"` + ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"` + ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"` + ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"` + PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"` + PerformanceInfoInterval int `default:"1" describe:"监控获取间隔时间(分钟)"` + UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"` + LogMinLenth int `default:"0" describe:"过滤日志最小长度"` + PprofEnable bool `default:"true" describe:"启用pprof分析工具"` +} diff --git a/consts/consts.go b/consts/consts.go new file mode 100644 index 0000000..289ea05 --- /dev/null +++ b/consts/consts.go @@ -0,0 +1 @@ +package consts \ No newline at end of file diff --git a/consts/ctxflag/ctxflag.go b/consts/ctxflag/ctxflag.go new file mode 100644 index 0000000..9717049 --- /dev/null +++ b/consts/ctxflag/ctxflag.go @@ -0,0 +1,7 @@ +package ctxflag + +const ( + USER_NAME = "user" + ROLE = "role" + ERR = "err" +) diff --git a/consts/permission/permission.go b/consts/permission/permission.go new file mode 100644 index 0000000..343acb1 --- /dev/null +++ b/consts/permission/permission.go @@ -0,0 +1,9 @@ +package permission + +type OprPermission string + +const ( + START_OPERATION OprPermission = "Start" + STOP_OPERATION OprPermission = "Stop" + TERMINAL_OPERATION OprPermission = "Terminal" +) diff --git a/consts/role/role.go b/consts/role/role.go new file mode 100644 index 0000000..ef9db76 --- /dev/null +++ b/consts/role/role.go @@ -0,0 +1,10 @@ +package role + +type Role int + +const ( + ROOT Role = iota + ADMIN + USER + GUEST +) diff --git a/dao/config.go b/dao/config.go new file mode 100644 index 0000000..579fc9d --- /dev/null +++ b/dao/config.go @@ -0,0 +1,30 @@ +package dao + +import ( + "msm/model" + + "gorm.io/gorm" +) + +type configDao struct{} + +var ConfigDao = new(configDao) + +func (c *configDao) GetConfigValue(key string) (string, error) { + var result string + if err := db.Model(&model.Config{}).Select("value").Where("key = ?", key).First(&result).Error; err != nil { + return "", err + } + return result, nil +} + +func (c *configDao) SetConfigValue(key, value string) error { + if db.Model(&model.Config{}).Where("key = ?", key).First(nil).Error == gorm.ErrRecordNotFound { + return db.Create(&model.Config{ + Key: key, + Value: value, + }).Error + } else { + return db.Model(&model.Config{}).Where("key = ?", key).Updates(model.Config{Value: value}).Error + } +} diff --git a/dao/db.go b/dao/db.go new file mode 100644 index 0000000..6034630 --- /dev/null +++ b/dao/db.go @@ -0,0 +1,40 @@ +package dao + +import ( + "log" + zlog "msm/log" + "msm/model" + "os" + "time" + + "github.com/glebarez/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var db *gorm.DB + +var defaultConfig = gorm.Session{PrepareStmt: true, SkipDefaultTransaction: true} + +func init() { + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Silent, + IgnoreRecordNotFoundError: true, + ParameterizedQueries: true, + Colorful: true, + }, + ) + gdb, err := gorm.Open(sqlite.Open("data.db"), &gorm.Config{ + Logger: newLogger, + }) + if err != nil { + zlog.Logger.Panicf("sqlite数据库初始化失败!\n错误原因:%v", err) + } + zlog.Logger.Info("sqlite初始化成功") + db = gdb.Session(&defaultConfig) + // db = gdb.Session(&defaultConfig).Debug() + db.AutoMigrate(&model.Process{}, &model.User{}, &model.Permission{}, &model.Push{}, &model.Config{}) +} diff --git a/dao/permission.go b/dao/permission.go new file mode 100644 index 0000000..472456f --- /dev/null +++ b/dao/permission.go @@ -0,0 +1,42 @@ +package dao + +import ( + "msm/log" + "msm/model" + + "gorm.io/gorm" +) + +type permissionDao struct{} + +var PermissionDao = new(permissionDao) + +func (p *permissionDao) GetPermssionList(account string) []model.PermissionPo { + result := []model.PermissionPo{} + if err := db.Raw(`SELECT p.name ,p.uuid as pid,p2.owned ,p2."start" ,p2.stop ,p2.terminal + FROM users u full join process p left join permission p2 on p2.account == u.account and p2.pid =p.uuid WHERE u.account = ? or u.account ISNULL`, account).Find(&result); err.Error != nil { + log.Logger.Warnw("权限查询失败", "err", err) + } + + return result +} + +func (p *permissionDao) EditPermssion(data model.Permission) error { + if db.Model(&model.Permission{}).Where("account = ? and pid = ?", data.Account, data.Pid).First(nil).Error == gorm.ErrRecordNotFound { + db.Omit("name").Create(&model.Permission{ + Account: data.Account, + Pid: data.Pid, + }) + } + return db.Debug().Model(&model.Permission{}).Where("account = ? and pid = ?", data.Account, data.Pid).Updates(map[string]interface{}{ + "owned": data.Owned, + "start": data.Start, + "stop": data.Stop, + "terminal": data.Terminal, + }).Error +} + +func (p *permissionDao) GetPermission(user string, pid int) (result model.Permission) { + db.Debug().Model(&model.Permission{}).Where("account = ? and pid = ?", user, pid).First(&result) + return +} diff --git a/dao/process.go b/dao/process.go new file mode 100644 index 0000000..1cf3313 --- /dev/null +++ b/dao/process.go @@ -0,0 +1,58 @@ +package dao + +import ( + "msm/log" + "msm/model" +) + +type processDao struct{} + +var ProcessDao = new(processDao) + +func (p *processDao) GetAllProcessConfig() []model.Process { + result := []model.Process{} + + tx := db.Find(&result) + if tx.Error != nil { + log.Logger.Error(tx.Error) + return []model.Process{} + } + return result +} + +func (p *processDao) GetProcessConfigByUser(username string) []model.Process { + result := []model.Process{} + tx := db.Debug().Raw(`SELECT p.uuid, p.name FROM permission left join process p where pid =p.uuid and owned = 1 and account = ?`, username).Scan(&result) + if tx.Error != nil { + log.Logger.Error(tx.Error) + return []model.Process{} + } + return result +} + +func (p *processDao) UpdateProcessConfig(process model.Process) error { + tx := db.Save(&process) + return tx.Error +} + +func (p *processDao) AddProcessConfig(process model.Process) (int, error) { + tx := db.Create(&process) + return process.Uuid, tx.Error +} + +func (p *processDao) DeleteProcessConfig(uuid int) error { + tx := db.Delete(&model.Process{ + Uuid: uuid, + }) + return tx.Error +} + +func (p *processDao) GetProcessConfigById(uuid int) model.Process { + result := model.Process{} + tx := db.Where(&model.Process{Uuid: uuid}).First(&result) + if tx.Error != nil { + log.Logger.Error(tx.Error) + return model.Process{} + } + return result +} diff --git a/dao/push.go b/dao/push.go new file mode 100644 index 0000000..3c88ebd --- /dev/null +++ b/dao/push.go @@ -0,0 +1,33 @@ +package dao + +import ( + "msm/model" +) + +type pushDao struct{} + +var PushDao = new(pushDao) + +func (p *pushDao) GetPushList() (result []model.Push) { + db.Find(&result) + return +} + +func (p *pushDao) GetPushConfigById(id int) (result model.Push) { + db.Where("id = ?", id).First(&result) + return +} + +func (p *pushDao) UpdatePushConfig(data model.Push) error { + return db.Save(&data).Error +} + +func (p *pushDao) AddPushConfig(data model.Push) error { + return db.Create(&data).Error +} + +func (p *pushDao) DeletePushConfig(id int) error { + return db.Delete(&model.Push{ + Id: int64(id), + }).Error +} diff --git a/dao/user.go b/dao/user.go new file mode 100644 index 0000000..bbb8342 --- /dev/null +++ b/dao/user.go @@ -0,0 +1,51 @@ +package dao + +import ( + "errors" + "msm/config" + "msm/model" + "msm/utils" + "time" +) + +type userDao struct{} + +var UserDao = new(userDao) + +func (u *userDao) GetUserByName(name string) model.User { + var result model.User + db.Where("account = ?", name).First(&result) + return result +} + +func (u *userDao) CreateUser(user model.User) error { + if len(user.Password) < config.CF.UserPassWordMinLength { + return errors.New("密码小于最小长度") + } + user.Password = utils.Md5(user.Password) + user.CreateTime = time.Now() + tx := db.Create(&user) + return tx.Error +} + +func (u *userDao) UpdatePassword(name string, password string) error { + if len(password) < config.CF.UserPassWordMinLength { + return errors.New("新密码太短") + } + tx := db.Model(&model.User{}).Where("account = ?", name).Update("password", utils.Md5(password)) + return tx.Error +} + +func (u *userDao) DeleteUser(name string) error { + if err := db.Where("account = ?", name).First(&model.User{}).Error; err != nil { + return err + } + tx := db.Delete(&model.User{Account: name}) + return tx.Error +} + +func (u *userDao) GetUserList() []model.User { + result := []model.User{} + db.Find(&result) + return result +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dad51d8 --- /dev/null +++ b/go.mod @@ -0,0 +1,83 @@ +module msm + +go 1.21.1 + +require github.com/gorilla/websocket v1.5.1 + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + gorm.io/gorm v1.25.7 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/elastic/elastic-transport-go/v8 v8.5.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/pprof v1.5.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/creack/pty v1.1.21 + github.com/elastic/go-elasticsearch/v8 v8.13.1 + github.com/gin-gonic/gin v1.9.1 + github.com/glebarez/sqlite v1.11.0 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d + github.com/panjf2000/ants v1.3.0 + github.com/shirou/gopsutil v3.21.11+incompatible + go.uber.org/zap v1.26.0 + golang.org/x/net v0.24.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..76162ad --- /dev/null +++ b/go.sum @@ -0,0 +1,212 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elastic/elastic-transport-go/v8 v8.5.0 h1:v5membAl7lvQgBTexPRDBO/RdnlQX+FM9fUVDyXxvH0= +github.com/elastic/elastic-transport-go/v8 v8.5.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= +github.com/elastic/go-elasticsearch/v8 v8.13.1 h1:du5F8IzUUyCkzxyHdrO9AtopcG95I/qwi2WK8Kf1xlg= +github.com/elastic/go-elasticsearch/v8 v8.13.1/go.mod h1:DIn7HopJs4oZC/w0WoJR13uMUxtHeq92eI5bqv5CRfI= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU= +github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d h1:8fVmm2qScPn4JAF/YdTtqrPP3n58FgZ4GbKTNfaPuRs= +github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d/go.mod h1:dFu6nuJHC3u9kCDcyGrEL7LwhK2m6Mt+alyiiIjDrRY= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/panjf2000/ants v1.3.0 h1:8pQ+8leaLc9lys2viEEr8md0U4RN6uOSUCE9bOYjQ9M= +github.com/panjf2000/ants v1.3.0/go.mod h1:AaACblRPzq35m1g3enqYcxspbbiOJJYaxU2wMpm1cXY= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..d08ef8d --- /dev/null +++ b/log/log.go @@ -0,0 +1,44 @@ +package log + +import ( + "log" + "msm/config" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var Logger *zap.SugaredLogger + +func init() { + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.FullCallerEncoder, + } + level, err := zapcore.ParseLevel(config.CF.LogLevel) + if err != nil { + log.Printf("日志等级错误!不存在“%v”日志等级", config.CF.LogLevel) + level = zap.DebugLevel + } + atom := zap.NewAtomicLevelAt(level) + zap.NewDevelopmentConfig() + config := zap.Config{ + Level: atom, + Development: true, + Encoding: "console", + EncoderConfig: encoderConfig, + OutputPaths: []string{"stdout", "info.log"}, + ErrorOutputPaths: []string{"stderr"}, + } + log, _ := config.Build() + Logger = log.Sugar() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9e0eb39 --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "msm/boot" + "msm/route" + + "github.com/gin-gonic/gin" +) + +func main() { + boot.Boot() + // go termui.TermuiInit() + gin.SetMode(gin.ReleaseMode) + route.Route() +} diff --git a/model/config.go b/model/config.go new file mode 100644 index 0000000..f49cc68 --- /dev/null +++ b/model/config.go @@ -0,0 +1,18 @@ +package model + +type Config struct { + Id int `gorm:"column:id;primary_key"` + Key string `gorm:"column:key"` + Value string `gorm:"column:value"` +} + +func (n *Config) TableName() string { + return "config" +} + +type SystemConfigurationResp struct { + Key string `json:"key"` + Value any `json:"value"` + Default string `json:"default"` + Describe string `json:"describe"` +} diff --git a/model/es.go b/model/es.go new file mode 100644 index 0000000..99a70cb --- /dev/null +++ b/model/es.go @@ -0,0 +1,103 @@ +package model + +type EsResult struct { + Took int `json:"took"` + TimedOut bool `json:"timed_out"` + Shards Shards `json:"_shards"` + Hits Hits `json:"hits"` +} +type Shards struct { + Total int `json:"total"` + Successful int `json:"successful"` + Skipped int `json:"skipped"` + Failed int `json:"failed"` +} +type Total struct { + Value int `json:"value"` + Relation string `json:"relation"` +} +type Source struct { + Log string `json:"log"` + Name string `json:"name"` + Time int64 `json:"time"` + Using string `json:"using"` +} +type HitsItem struct { + Index string `json:"_index"` + ID string `json:"_id"` + Score interface{} `json:"_score"` + Source Source `json:"_source"` + Sort []int64 `json:"sort"` +} +type Hits struct { + Total Total `json:"total"` + MaxScore interface{} `json:"max_score"` + Hits []HitsItem `json:"hits"` +} + +type GetLogReq struct { + Match struct { + Log string `json:"log"` + Name string `json:"name"` + Using string `json:"using"` + } `json:"match"` + TimeRange struct { + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` + } `json:"time"` + Page struct { + From int `json:"from"` + Size int `json:"size"` + } `json:"page"` + Sort string `json:"sort"` +} + +type EsResp struct { + Took int `json:"took"` + TimedOut bool `json:"timed_out"` + Shards struct { + Total int `json:"total"` + Successful int `json:"successful"` + Skipped int `json:"skipped"` + Failed int `json:"failed"` + } `json:"_shards"` + Hits struct { + Total struct { + Value int `json:"value"` + Relation string `json:"relation"` + } `json:"total"` + MaxScore int `json:"max_score"` + Hits []struct { + Index string `json:"_index"` + ID string `json:"_id"` + Score int `json:"_score"` + Source struct { + Log string `json:"log"` + Name string `json:"name"` + Time int64 `json:"time"` + Using string `json:"using"` + } `json:"_source"` + } `json:"hits"` + } `json:"hits"` +} + +type LogResp struct { + Total int `json:"total"` + Data []Eslog `json:"data"` +} + +type Eslog struct { + Log string `json:"log"` + Time int64 `json:"time"` + Name string `json:"name"` + Using string `json:"using"` + Id string `json:"id"` +} + +type QueryBody struct { + Query struct { + Bool struct { + Must []any `json:"must"` + } `json:"bool"` + } `json:"query"` +} diff --git a/model/file.go b/model/file.go new file mode 100644 index 0000000..d0f12e7 --- /dev/null +++ b/model/file.go @@ -0,0 +1,6 @@ +package model + +type FileStruct struct { + Name string `json:"name"` + IsDir bool `json:"isDir"` +} diff --git a/model/message.go b/model/message.go new file mode 100644 index 0000000..5f542c5 --- /dev/null +++ b/model/message.go @@ -0,0 +1,8 @@ +package model + + + +type WsMessage struct { + MessageType string `json:"messageType"` + Content string `json:"content"` +} diff --git a/model/permission.go b/model/permission.go new file mode 100644 index 0000000..448fbab --- /dev/null +++ b/model/permission.go @@ -0,0 +1,26 @@ +package model + +type Permission struct { + Id int64 `gorm:"column:id;NOT NULL" json:"id"` + Account string `gorm:"column:account;NOT NULL" json:"account"` + Pid int32 `gorm:"column:pid;NOT NULL" json:"pid"` + Owned bool `gorm:"column:owned;NOT NULL" json:"owned"` + Start bool `gorm:"column:start;NOT NULL" json:"start"` + Stop bool `gorm:"column:stop;NOT NULL" json:"stop"` + Terminal bool `gorm:"column:terminal;NOT NULL" json:"terminal"` +} + +func (*Permission) TableName() string { + return "permission" +} + +type PermissionPo struct { + Id int64 `gorm:"column:id" json:"id"` + Account string `gorm:"column:account" json:"account"` + Name string `gorm:"column:name" json:"name"` + Pid int32 `gorm:"column:pid" json:"pid"` + Owned bool `gorm:"column:owned" json:"owned"` + Start bool `gorm:"column:start" json:"start"` + Stop bool `gorm:"column:stop" json:"stop"` + Terminal bool `gorm:"column:terminal" json:"terminal"` +} diff --git a/model/proc.go b/model/proc.go new file mode 100644 index 0000000..de0b144 --- /dev/null +++ b/model/proc.go @@ -0,0 +1,22 @@ +package model + +type ProcessInfo struct { + Name string `json:"name"` + Uuid int `json:"uuid"` + StartTime string `json:"startTime"` + User string `json:"user"` + Usage Usage `json:"usage"` + State State `json:"state"` + TermType string `json:"termType"` +} + +type Usage struct { + Cpu []float64 `json:"cpu"` + Mem []float64 `json:"mem"` + Time []string `json:"time"` +} + +type State struct { + State uint8 `json:"state"` + Info string `json:"info"` +} diff --git a/model/process.go b/model/process.go new file mode 100644 index 0000000..16ac218 --- /dev/null +++ b/model/process.go @@ -0,0 +1,16 @@ +package model + +type Process struct { + Uuid int `gorm:"primaryKey;autoIncrement;column:uuid" json:"uuid"` + Name string `gorm:"column:name" json:"name"` + Cmd string `gorm:"column:args" json:"cmd"` + Cwd string `gorm:"column:cwd" json:"cwd"` + AutoRestart bool `gorm:"column:auto_restart" json:"autoRestart"` + Push bool `gorm:"column:push" json:"push"` + LogReport bool `gorm:"column:log_report" json:"logReport"` + TermType string `gorm:"column:term_type" json:"termType"` +} + +func (*Process) TableName() string { + return "process" +} diff --git a/model/push_msg.go b/model/push_msg.go new file mode 100644 index 0000000..090dc9c --- /dev/null +++ b/model/push_msg.go @@ -0,0 +1,14 @@ +package model + +type Push struct { + Id int64 `gorm:"column:id;NOT NULL" json:"id"` + Method string `gorm:"column:method;NOT NULL" json:"method"` + Url string `gorm:"column:url;NOT NULL" json:"url"` + Body string `gorm:"column:body;NOT NULL" json:"body"` + Remark string `gorm:"column:remark;NOT NULL" json:"remark"` + Enable bool `gorm:"column:enable;NOT NULL" json:"enable"` +} + +func (*Push) TableName() string { + return "push" +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..538f713 --- /dev/null +++ b/model/user.go @@ -0,0 +1,15 @@ +package model + +import "time" + +type User struct { + Account string `json:"account" gorm:"primaryKey;column:account" ` + Password string `json:"password" gorm:"column:password" ` + Role int `json:"role" gorm:"column:role" ` + CreateTime time.Time `json:"createTime" gorm:"column:create_time" ` + Remark string `json:"remark" gorm:"column:remark" ` +} + +func (*User) TableName() string { + return "users" +} diff --git a/route/middle/panic.go b/route/middle/panic.go new file mode 100644 index 0000000..5fd8b65 --- /dev/null +++ b/route/middle/panic.go @@ -0,0 +1,26 @@ +package middle + +import ( + "msm/consts/ctxflag" + + "github.com/gin-gonic/gin" +) + +func PanicMiddle() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err == 0 { + if err, ok := c.Get(ctxflag.ERR); ok { + rErr(c, -1, err.(error).Error(), err.(error)) + } else { + rErr(c, -1, "内部错误", nil) + } + } else { + if err != nil { + panic(err) + } + } + }() + c.Next() + } +} diff --git a/route/middle/permission.go b/route/middle/permission.go new file mode 100644 index 0000000..35ced67 --- /dev/null +++ b/route/middle/permission.go @@ -0,0 +1,44 @@ +package middle + +import ( + "msm/consts/ctxflag" + "msm/consts/permission" + "msm/consts/role" + "msm/dao" + "reflect" + "strconv" + + "github.com/gin-gonic/gin" +) + +func RolePermission(needPermission role.Role) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + if r := ctx.GetInt(ctxflag.ROLE); r > int(needPermission) { + rErr(ctx, -1, "角色权限不足", nil) + ctx.Abort() + return + } + ctx.Next() + } +} + +func OprPermission(op permission.OprPermission) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + uuid, err := strconv.Atoi(ctx.Query("uuid")) + if err != nil { + rErr(ctx, -1, "参数有误", nil) + ctx.Abort() + return + } + if ctx.GetInt(ctxflag.ROLE) < int(role.USER) { + ctx.Next() + return + } + if !reflect.ValueOf(dao.PermissionDao.GetPermission(ctx.GetString(ctxflag.USER_NAME), uuid)).FieldByName(string(op)).Bool() { + rErr(ctx, -1, "操作权限不足", nil) + ctx.Abort() + return + } + ctx.Next() + } +} diff --git a/route/middle/token.go b/route/middle/token.go new file mode 100644 index 0000000..e6ca3ca --- /dev/null +++ b/route/middle/token.go @@ -0,0 +1,73 @@ +package middle + +import ( + "errors" + "msm/consts/ctxflag" + "msm/dao" + "msm/log" + "msm/utils" + "slices" + + "github.com/gin-gonic/gin" +) + +// code -1为失败,-2为token失效 +func rErr(ctx *gin.Context, code int, message string, err error) { + var statusCode int + switch code { + case -1: + statusCode = 500 + case -2: + statusCode = 401 + default: + statusCode = 200 + } + log.Logger.Warn(err) + ctx.JSON(statusCode, map[string]any{ + "code": code, + "msg": message, + }) + ctx.Abort() +} + +func CheckToken() gin.HandlerFunc { + return func(c *gin.Context) { + whiteList := []string{ + "/api/user/login", + "/api/user/register/admin", + } + if !slices.Contains(whiteList, c.Request.URL.Path) { + var token string + if c.Request.Header.Get("token") != "" { + token = c.Request.Header.Get("token") + } else { + token = c.Query("token") + } + if _, err := utils.ParseToken(token); err != nil { + rErr(c, -2, "token校验失败", err) + return + } + if username, err := getUser(c); err != nil { + rErr(c, -1, "无法获取user信息", err) + } else { + c.Set(ctxflag.USER_NAME, username) + c.Set(ctxflag.ROLE, dao.UserDao.GetUserByName(username).Role) + } + } + c.Next() + } +} + +func getUser(ctx *gin.Context) (string, error) { + var token string + if ctx.Request.Header.Get("token") != "" { + token = ctx.Request.Header.Get("token") + } else { + token = ctx.Query("token") + } + if mc, err := utils.ParseToken(token); err == nil && mc != nil { + return mc.UserName, nil + } else { + return "", errors.Join(errors.New("用户信息获取失败"), err) + } +} diff --git a/route/route.go b/route/route.go new file mode 100644 index 0000000..85cfd55 --- /dev/null +++ b/route/route.go @@ -0,0 +1,111 @@ +package route + +import ( + "io" + "msm/api" + "msm/config" + "msm/consts/permission" + "msm/consts/role" + "msm/log" + "msm/route/middle" + "net/http" + + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" +) + +func Route() { + r := gin.Default() + gin.DefaultWriter = io.Discard + gin.SetMode(gin.DebugMode) + routePathInit(r) + staticInit(r) + pprofInit(r) + r.Run(config.CF.Listen) +} + +func staticInit(r *gin.Engine) { + r.NoRoute(func(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) + }) + r.Static("/js", "templates/js") + r.Static("/css", "templates/css") + r.Static("/media", "templates/media") + r.Static("/fonts", "templates/fonts") + r.LoadHTMLFiles("templates/index.html") +} + +func pprofInit(r *gin.Engine) { + if config.CF.PprofEnable { + pprof.Register(r) + log.Logger.Info("启用 pprof") + } +} + +func routePathInit(r *gin.Engine) { + apiGroup := r.Group("/api") + apiGroup.Use(middle.CheckToken()) + apiGroup.Use(middle.PanicMiddle()) + { + apiGroup.GET("/ws", middle.OprPermission(permission.TERMINAL_OPERATION), api.WsApi.WebsocketHandle) + + processGroup := apiGroup.Group("/process") + { + processGroup.DELETE("", middle.OprPermission(permission.STOP_OPERATION), api.ProcApi.KillProcess) + processGroup.GET("", api.ProcApi.GetProcessList) + processGroup.PUT("", middle.OprPermission(permission.START_OPERATION), api.ProcApi.StartProcess) + processGroup.GET("/control", middle.RolePermission(role.ADMIN), api.ProcApi.ProcessControl) + + proConfigGroup := processGroup.Group("/config") + { + proConfigGroup.POST("", middle.RolePermission(role.ROOT), api.ProcApi.CreateNewProcess) + proConfigGroup.DELETE("", middle.RolePermission(role.ROOT), api.ProcApi.DeleteNewProcess) + proConfigGroup.PUT("", middle.RolePermission(role.ROOT), api.ProcApi.UpdateProcessConfig) + proConfigGroup.GET("", middle.RolePermission(role.ADMIN), api.ProcApi.GetProcessConfig) + } + } + + userGroup := apiGroup.Group("/user") + { + userGroup.POST("/login", api.UserApi.LoginHandler) + userGroup.POST("", middle.RolePermission(role.ROOT), api.UserApi.CreateUser) + userGroup.PUT("/password", middle.RolePermission(role.USER), api.UserApi.ChangePassword) + userGroup.DELETE("", middle.RolePermission(role.ROOT), api.UserApi.DeleteUser) + userGroup.GET("", middle.RolePermission(role.ROOT), api.UserApi.GetUserList) + } + + pushGroup := apiGroup.Group("/push").Use(middle.RolePermission(role.ADMIN)) + { + pushGroup.GET("/list", api.PushApi.GetPushList) + pushGroup.GET("", api.PushApi.GetPushById) + pushGroup.POST("", api.PushApi.AddPushConfig) + pushGroup.PUT("", api.PushApi.UpdatePushConfig) + pushGroup.DELETE("", api.PushApi.DeletePushConfig) + } + + fileGroup := apiGroup.Group("/file").Use(middle.RolePermission(role.ADMIN)) + { + fileGroup.GET("/list", api.FileApi.FilePathHandler) + fileGroup.PUT("", api.FileApi.FileWriteHandler) + fileGroup.GET("", api.FileApi.FileReadHandler) + } + + permissionGroup := apiGroup.Group("/permission").Use(middle.RolePermission(role.ROOT)) + { + permissionGroup.GET("/list", api.PermissionApi.GetPermissionList) + permissionGroup.PUT("", api.PermissionApi.EditPermssion) + } + + logGroup := apiGroup.Group("/log").Use(middle.RolePermission(role.ADMIN)) + { + logGroup.POST("", api.LogApi.GetLog) + } + + configGroup := apiGroup.Group("/config").Use(middle.RolePermission(role.ROOT)) + { + configGroup.GET("", api.ConfigApi.GetSystemConfiguration) + configGroup.PUT("", api.ConfigApi.SetSystemConfiguration) + configGroup.PUT("/es", api.ConfigApi.EsConfigReload) + } + } +} diff --git a/server_config.json b/server_config.json new file mode 100644 index 0000000..3a72672 --- /dev/null +++ b/server_config.json @@ -0,0 +1,128 @@ +{ + "user": [ + { + "account": "admin", + "password": "91cd3b960f31c06fc4048ff44e6654c1" + }, + { + "account": "user", + "password": "5f4dcc3b5aa765d61d8327deb882cf99" + } + ], + "server": [ + { + "name": "bungeecord", + "args": [ + "/usr/lib/jvm/java-17-openjdk-amd64/bin/java", + "-Xmx580M", + "-Xms100M", + "-jar", + "waterfall-1.19-510.jar" + ], + "cwd": "/MCS/BungeeCord", + "autoRestart": true, + "push": true, + "logReport": true + }, + { + "name": "lobby", + "args": [ + "java", + "-jar", + "-server", + "-Xmx1000M", + "paper-1.16.5-794.jar" + ], + "cwd": "/MCS/lobby", + "autoRestart": true, + "push": true, + "logReport": true + }, + { + "name": "main", + "args": [ + "java", + "-jar", + "-server", + "-Xmx8000M", + "launcher-airplane.jar" + ], + "cwd": "/MCS", + "autoRestart": true, + "push": true, + "logReport": true + }, + { + "name": "s1", + "args": [ + "java", + "-jar", + "-server", + "-Xmx5000M", + "launcher-airplane.jar" + ], + "cwd": "/MCS/server1", + "autoRestart": true, + "push": true, + "logReport": true + }, + { + "name": "起床战争bungeecord", + "args": [ + "java", + "-jar", + "BB.jar" + ], + "cwd": "/MCS/bed/[25565]BungeeCord", + "autoRestart": true + }, + { + "name": "起床战争looby", + "args": [ + "java", + "-jar", + "paper-1.16.5-794.jar" + ], + "cwd": "/MCS/bed/[25566]Lobby", + "autoRestart": true + }, + { + "name": "起床战争入侵", + "args": [ + "/usr/lib/jvm/java-8-openjdk-amd64/bin/java", + "-jar", + "RQS.jar" + ], + "cwd": "/MCS/bed/[20003]BedWars-入侵", + "autoRestart": true + }, + { + "name": "起床战争蘑菇", + "args": [ + "/usr/lib/jvm/java-8-openjdk-amd64/bin/java", + "-jar", + "PaperSpigot-1.8.8.jar" + ], + "cwd": "/MCS/bed/[10002]BedWarsXP-蘑菇", + "autoRestart": true + } + ], + "mq": { + "enable": true, + "mqurl": "amqp://admin:1625167628%40xcon@xcon.top:5672/", + "queue_name": "log_queue_publisher", + "exchange": "log_exchange_publisher", + "routing_key": "server_log" + }, + "push": { + "feishu": { + "enable": true, + "webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/86f491ba-6c7a-413b-86e6-d72420934bd9" + }, + "wechat":{ + "enable": true, + "webhook": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=5f3fae11-da5c-45a7-8ae9-ceaca2dc1495" + } + }, + "logLevel": "debug" +} \ No newline at end of file diff --git a/service/es/es.go b/service/es/es.go new file mode 100644 index 0000000..7809cdf --- /dev/null +++ b/service/es/es.go @@ -0,0 +1,155 @@ +package es + +import ( + "bytes" + "context" + "encoding/json" + "msm/config" + "msm/log" + "msm/model" + + "github.com/elastic/go-elasticsearch/v8" + "github.com/elastic/go-elasticsearch/v8/esapi" +) + +var esClient *elasticsearch.Client + +type esService struct{} + +var EsService = new(esService) + +func InitEs() bool { + if config.CF.EsEnable { + cfg := elasticsearch.Config{ + Addresses: []string{ + config.CF.EsUrl, + }, + Username: config.CF.EsUsername, + Password: config.CF.EsPassword, + } + var err error + esClient, err = elasticsearch.NewClient(cfg) + if err != nil { + log.Logger.Fatalln("Failed to connect to es") + } + _, err = esClient.Info() + if err != nil { + log.Logger.Error("es启动失败", err) + config.CF.EsEnable = false + } else { + return true + } + } else { + log.Logger.Debug("不使用es") + } + return false +} + +// idx 为空,默认随机唯一字符串 +func (e *esService) Index(index, idx string, doc map[string]interface{}) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(doc); err != nil { + log.Logger.Error(err, "Error encoding doc") + return + } + res, err := esClient.Index( + index, + &buf, + esClient.Index.WithDocumentID(idx), + esClient.Index.WithRefresh("true"), + ) + if err != nil { + log.Logger.Error(err, "Error create response") + } + defer res.Body.Close() +} + +func (e *esService) Insert(log string, processName string, using string, ts int64) { + doc := map[string]interface{}{ + "log": log, + "name": processName, + "using": using, + "time": ts, + } + e.Index(config.CF.EsIndex, "", doc) +} + +func (e *esService) Search(req model.GetLogReq) model.LogResp { + query := []func(*esapi.SearchRequest){ + esClient.Search.WithIndex(config.CF.EsIndex), + esClient.Search.WithContext(context.Background()), + esClient.Search.WithPretty(), + esClient.Search.WithTrackTotalHits(true), + esClient.Search.WithFrom(req.Page.From), + esClient.Search.WithSize(req.Page.Size), + } + if req.Sort == "asc" { + query = append(query, esClient.Search.WithSort("time:asc")) + } + if req.Sort == "desc" { + query = append(query, esClient.Search.WithSort("time:desc")) + } + body := e.buildQueryBody(req) + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(body); err != nil { + log.Logger.Error(err) + return model.LogResp{} + } + query = append(query, esClient.Search.WithBody(&buf)) + res, err := esClient.Search(query...) + if err != nil { + log.Logger.Error(err) + return model.LogResp{} + } + resp := model.EsResp{} + json.NewDecoder(res.Body).Decode(&resp) + res.Body.Close() + result := model.LogResp{} + for _, v := range resp.Hits.Hits { + result.Data = append(result.Data, model.Eslog{ + Log: v.Source.Log, + Name: v.Source.Name, + Using: v.Source.Using, + Time: v.Source.Time, + Id: v.ID, + }) + } + result.Total = resp.Hits.Total.Value + return result +} + +func (e *esService) buildQueryBody(req model.GetLogReq) model.QueryBody { + result := model.QueryBody{} + if req.TimeRange.EndTime != 0 || req.TimeRange.StartTime != 0 { + result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{ + "range": map[string]any{ + "time": map[string]any{ + "gte": req.TimeRange.StartTime, + "lte": req.TimeRange.EndTime, + }, + }, + }) + } + if req.Match.Log != "" { + result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{ + "match": map[string]any{ + "log": req.Match.Log, + }, + }) + } + if req.Match.Name != "" { + result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{ + "match": map[string]any{ + "name": req.Match.Name, + }, + }) + } + if req.Match.Using != "" { + result.Query.Bool.Must = append(result.Query.Bool.Must, map[string]any{ + "match": map[string]any{ + "using": req.Match.Using, + }, + }) + } + return result +} diff --git a/service/file/file.go b/service/file/file.go new file mode 100644 index 0000000..955932b --- /dev/null +++ b/service/file/file.go @@ -0,0 +1,76 @@ +package file + +import ( + "fmt" + "io" + "msm/config" + "msm/log" + "msm/model" + "os" +) + +type fileService struct{} + +var FileService = new(fileService) + +func (f *fileService) ReadFileFromPath(path string) (result []byte, err error) { + fi, err := os.Open(path) + if err != nil { + return + } + defer fi.Close() + fileInfo, err := fi.Stat() + if err != nil { + return + } + if size := float64(fileInfo.Size()) / 1e6; size > config.CF.FileSizeLimit { + err = fmt.Errorf("写入数据大小%vMB,超过%vMB限制", size, config.CF.FileSizeLimit) + return + } + result, err = io.ReadAll(fi) + if err != nil { + return + } + log.Logger.Debugw("文件写入成功", "path", path) + return +} + +func (f *fileService) UpdateFileData(filePath string, file io.Reader, size int64) error { + if size := float64(size) / 1e6; size > config.CF.FileSizeLimit { + return fmt.Errorf("写入数据大小%vMB,超过%vMB限制", size, config.CF.FileSizeLimit) + } + fi, err := os.OpenFile(filePath, os.O_RDWR|os.O_TRUNC, 0777) + if err != nil { + return err + } + defer fi.Close() + if _, err = io.Copy(fi, file); err != nil { + return err + } + log.Logger.Debugw("文件写入成功", "path", filePath) + return nil +} + +func (f *fileService) GetFileAndDirByPath(srcPath string) ([]model.FileStruct, error) { + result := []model.FileStruct{} + files, err := os.ReadDir(srcPath) + if err != nil { + return result, err + } + for _, file := range files { + result = append(result, model.FileStruct{ + Name: file.Name(), + IsDir: file.IsDir(), + }) + } + return result, nil +} + +func (f *fileService) CreateNewDir(path string, name string) error { + _, err := os.Create(path + name) + return err +} + +func (f *fileService) CreateNewFile(path string, name string) error { + return os.MkdirAll(path+name, os.ModeDir) +} diff --git a/service/log/loghandler.go b/service/log/loghandler.go new file mode 100644 index 0000000..c084b49 --- /dev/null +++ b/service/log/loghandler.go @@ -0,0 +1,40 @@ +package loghandler + +import ( + "msm/log" + "msm/model" + "msm/service/es" + "time" + + "github.com/panjf2000/ants" +) + +type loghandler struct{} + +var ( + antsPool *ants.PoolWithFunc + Loghandler = new(loghandler) + + logHanleFunc = func(i interface{}) { + esLog, ok := i.(model.Eslog) + if !ok { + log.Logger.Panicw("传入错误参数", "data", esLog) + return + } + es.EsService.Insert(esLog.Log, esLog.Name, esLog.Using, esLog.Time) + } + + panicHanlderFunc = func(i interface{}) { + log.Logger.Error("es消息储存失败") + } +) + +func init() { + antsPool, _ = ants.NewPoolWithFunc(1000, logHanleFunc, ants.WithPanicHandler(panicHanlderFunc), ants.WithExpiryDuration(time.Second*10)) +} + +func (l *loghandler) AddLog(data model.Eslog) { + if err := antsPool.Invoke(data); err != nil { + log.Logger.Errorw("协程池添加任务失败", "err", err, "当前运行数量", antsPool.Running()) + } +} diff --git a/service/process/proccess.go b/service/process/proccess.go new file mode 100644 index 0000000..225ee92 --- /dev/null +++ b/service/process/proccess.go @@ -0,0 +1,324 @@ +package process + +import ( + "errors" + "msm/config" + "msm/log" + "msm/model" + loghandler "msm/service/log" + "msm/service/push" + "os/exec" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" + pu "github.com/shirou/gopsutil/process" +) + +type Process interface { + ReadCache(*websocket.Conn) + GetName() string + SetName(string) + GetTermType() string + SetTermType(string) + SetIsUsing(bool) + GetWhoUsing() string + SetWhoUsing(string) + SetStartCommand([]string) + GetControlController() string + SetControlController(string) + ChangControlChan() chan int + StopChan() chan struct{} + SetConfigLogReport(bool) + SetConfigStatuPush(bool) + SetConfigAutoRestart(bool) + GetStateInfo() string + GetStateState() uint8 + Kill() error + SetWsConn(*websocket.Conn) + Write(string) error + WriteBytes([]byte) error + GetStartTimeFormat() string + VerifyControl() bool + ResetRestartTimes() + InitPerformanceStatus() + ProcessControl(string) + AddCpuUsage(float64) + AddMemUsage(float64) + AddRecordTime() + GetTimeRecord() []string + GetMemUsage() []float64 + GetCpuUsage() []float64 + monitorHanler() + initPsutil() + SetAutoRestart(bool) + TryLock() bool + Unlock() + ReStart() +} + +type ProcessBase struct { + Name string + termType string + Pid int + cmd *exec.Cmd + IsUsing atomic.Bool + StartCommand []string + Lock sync.Mutex + WhoUsing string + stopChan chan struct{} + Control struct { + Controller string + changControlChan chan int + changControlTime time.Time + } + ws struct { + wsConnect *websocket.Conn + wsMux sync.RWMutex + } + Config struct { + AutoRestart bool + statuPush bool + logReport bool + } + State struct { + startTime time.Time + Info string + State uint8 //0 为未运行,1为运作中,2为异常状态 + restartTimes int + } + performanceStatus struct { + cpu []float64 + mem []float64 + time []string + } + monitor struct { + enable bool + ProcessBase *pu.Process + } +} + +func (p *ProcessBase) GetTermType() string { + return p.termType +} + +func (p *ProcessBase) SetTermType(s string) { + p.termType = s +} + +func (p *ProcessBase) GetStateInfo() string { + return p.State.Info +} + +func (p *ProcessBase) GetStateState() uint8 { + return p.State.State +} + +func (p *ProcessBase) SetAutoRestart(data bool) { + p.Config.AutoRestart = data +} + +func (p *ProcessBase) GetWhoUsing() string { + return p.WhoUsing +} + +func (p *ProcessBase) GetControlController() string { + return p.Control.Controller +} + +func (p *ProcessBase) SetControlController(c string) { + p.Control.Controller = c +} + +func (p *ProcessBase) SetWsConn(ws *websocket.Conn) { + p.ws.wsConnect = ws +} + +func (p *ProcessBase) logReportHandler(log string) { + if config.CF.EsEnable && p.Config.logReport && len([]rune(log)) > config.CF.LogMinLenth { + loghandler.Loghandler.AddLog(model.Eslog{ + Log: log, + Using: p.WhoUsing, + Name: p.Name, + Time: time.Now().UnixMilli(), + }) + } +} + +func (p *ProcessBase) GetStartTimeFormat() string { + return p.State.startTime.Format(time.DateTime) +} + +func (p *ProcessBase) ProcessControl(name string) { + p.Control.changControlTime = time.Now() + p.Control.Controller = name + if p.State.State == 1 && p.IsUsing.Load() { + p.Control.changControlChan <- 0 + } +} + +// 没人在使用或控制时间过期 +func (p *ProcessBase) VerifyControl() bool { + return p.Control.Controller == "" || p.Control.changControlTime.Unix() < time.Now().Unix()-config.CF.ProcessExpireTime +} + +func (p *ProcessBase) setProcessConfig(pconfig model.Process) { + p.Config.AutoRestart = pconfig.AutoRestart + p.Config.logReport = pconfig.LogReport + p.Config.statuPush = pconfig.Push +} + +func (p *ProcessBase) ResetRestartTimes() { + p.State.restartTimes = 0 +} + +func (p *ProcessBase) push(message string) { + if p.Config.statuPush { + messagePlaceholders := map[string]string{ + "{$name}": p.Name, + "{$user}": p.WhoUsing, + "{$message}": message, + "{$status}": strconv.Itoa(int(p.State.State)), + } + push.PushService.Push(messagePlaceholders) + } +} + +func (p *ProcessBase) InitPerformanceStatus() { + p.performanceStatus.cpu = make([]float64, config.CF.PerformanceInfoListLength) + p.performanceStatus.mem = make([]float64, config.CF.PerformanceInfoListLength) + p.performanceStatus.time = make([]string, config.CF.PerformanceInfoListLength) +} + +func (p *ProcessBase) AddCpuUsage(usage float64) { + p.performanceStatus.cpu = append(p.performanceStatus.cpu[1:], usage) +} + +func (p *ProcessBase) AddMemUsage(usage float64) { + p.performanceStatus.mem = append(p.performanceStatus.mem[1:], usage) +} + +func (p *ProcessBase) AddRecordTime() { + p.performanceStatus.time = append(p.performanceStatus.time[1:], time.Now().Format(time.DateTime)) +} +func (p *ProcessBase) GetCpuUsage() []float64 { + return p.performanceStatus.cpu +} + +func (p *ProcessBase) GetMemUsage() []float64 { + return p.performanceStatus.mem +} + +func (p *ProcessBase) GetTimeRecord() []string { + return p.performanceStatus.time +} + +func (p *ProcessBase) monitorHanler() { + defer log.Logger.Infow("性能监控结束", "name", p.Name, "pid", p.Pid) + for { + if !p.monitor.enable { + return + } + select { + case <-time.After(time.Minute * time.Duration(config.CF.PerformanceInfoInterval)): + if p.State.State != 1 { + log.Logger.Debugw("进程状态异常,跳过监控数据获取", "name", p.Name) + p.AddCpuUsage(0) + p.AddMemUsage(0) + p.AddRecordTime() + continue + } + ProcessBase := p.monitor.ProcessBase + cpuPercent, err := ProcessBase.CPUPercent() + if err != nil { + log.Logger.Errorw("CPU使用率获取失败", "err", err) + return + } + memInfo, err := ProcessBase.MemoryInfo() + if err != nil { + log.Logger.Errorw("内存使用率获取失败", "err", err) + return + } + p.AddRecordTime() + p.AddCpuUsage(cpuPercent) + p.AddMemUsage(float64(memInfo.RSS / 1000)) + log.Logger.Debugw("进程资源使用率获取成功", "pid", p.Pid, "name", p.Name, "cpu", cpuPercent, "mem", memInfo.RSS) + case <-p.stopChan: + return + } + } +} + +func (p *ProcessBase) initPsutil() { + pup, err := pu.NewProcess(int32(p.Pid)) + if err != nil { + p.monitor.enable = false + log.Logger.Debug("pu进程获取失败") + } else { + p.monitor.enable = true + log.Logger.Debug("pu进程获取成功") + p.monitor.ProcessBase = pup + } +} + +func (p *ProcessBase) SetConfigLogReport(b bool) { + p.Config.logReport = b +} + +func (p *ProcessBase) SetConfigAutoRestart(b bool) { + p.Config.AutoRestart = b +} + +func (p *ProcessBase) SetConfigStatuPush(b bool) { + p.Config.statuPush = b +} + +func (p *ProcessBase) SetName(s string) { + p.Name = s +} + +func (p *ProcessBase) SetStartCommand(cmd []string) { + p.StartCommand = cmd +} + +func (p *ProcessBase) ChangControlChan() chan int { + return p.Control.changControlChan +} + +func (p *ProcessBase) SetIsUsing(b bool) { + p.IsUsing.Store(b) +} + +func (p *ProcessBase) GetName() string { + return p.Name +} + +func (p *ProcessBase) SetWhoUsing(s string) { + p.WhoUsing = s +} + +func (p *ProcessBase) StopChan() chan struct{} { + return p.stopChan +} + +func (p *ProcessBase) TryLock() bool { + return p.Lock.TryLock() +} + +func (p *ProcessBase) Unlock() { + p.Lock.Unlock() +} + +func RunNewProcess(config model.Process) (proc Process, err error) { + switch config.TermType { + case "std": + proc, err = RunNewProcessStd(config) + case "pty": + proc, err = RunNewProcessPty(config) + default: + err = errors.New("终端类型错误") + } + return +} diff --git a/service/process/process_pty.go b/service/process/process_pty.go new file mode 100644 index 0000000..d8094b4 --- /dev/null +++ b/service/process/process_pty.go @@ -0,0 +1,169 @@ +package process + +import ( + "bytes" + "fmt" + "msm/config" + "msm/log" + "msm/model" + "msm/utils" + "os" + "os/exec" + "strings" + "time" + + "github.com/creack/pty" + "github.com/gorilla/websocket" +) + +type ProcessPty struct { + ProcessBase + cacheBytesBuf *bytes.Buffer + pty *os.File +} + +func (p *ProcessPty) Kill() error { + if err := p.cmd.Process.Kill(); err != nil { + log.Logger.Errorw("进程杀死失败", "err", err, "state", p.State.State) + return err + } + return p.pty.Close() +} + +func (p *ProcessPty) watchDog() { + state, _ := p.cmd.Process.Wait() + close(p.stopChan) + p.State.State = 0 + p.pty.Close() + if state.ExitCode() != 0 { + log.Logger.Infow("进程停止", "进程名称", p.Name, "exitCode", state.ExitCode(), "进程类型", "pty") + p.push(fmt.Sprintf("进程停止,退出码 %d", state.ExitCode())) + if p.Config.AutoRestart { + p.ReStart() + } + } else { + log.Logger.Infow("进程正常退出", "进程名称", p.Name) + p.push("进程正常退出") + } +} + +func (p *ProcessPty) ReStart() { + if p.State.restartTimes > config.CF.ProcessRestartsLimit { + log.Logger.Warnw("重启次数达到上限", "name", p.Name, "limit", config.CF.ProcessRestartsLimit) + p.State.State = 2 + p.State.Info = "重启次数异常" + p.push("进程重启次数达到上限") + return + } + cmd := exec.Command(p.StartCommand[0], p.StartCommand[1:]...) + cmd.Dir = p.cmd.Dir + pf, err := pty.Start(cmd) + if err != nil || p.cmd.Process == nil { + log.Logger.Error("进程启动出错:", err) + return + } + pty.Setsize(pf, &pty.Winsize{ + Rows: 100, + Cols: 100, + }) + p.pty = pf + p.State.restartTimes++ + log.Logger.Infow("进程启动成功", "进程名称", p.Name, "重启次数", p.State.restartTimes) + p.cmd = cmd + p.pInit() + p.push("进程启动成功") +} + +func (p *ProcessPty) WriteBytes(input []byte) (err error) { + p.logReportHandler(config.CF.ProcessInputPrefix + string(input)) + _, err = p.pty.Write(input) + return +} + +func (p *ProcessPty) Write(input string) (err error) { + p.logReportHandler(config.CF.ProcessInputPrefix + input) + _, err = p.pty.Write([]byte(input)) + return +} + +func (p *ProcessPty) readInit() { + log.Logger.Debugw("stdout读取线程已启动", "进程名", p.Name, "使用者", p.WhoUsing) + buf := make([]byte, 1024) + for { + select { + case <-p.stopChan: + { + p.IsUsing.Store(false) + p.WhoUsing = "" + log.Logger.Debugw("stdout读取线程已退出", "进程名", p.Name, "使用者", p.WhoUsing) + return + } + default: + { + n, _ := p.pty.Read(buf) + p.bufHanle(buf[:n]) + if p.IsUsing.Load() { + p.ws.wsMux.Lock() + p.ws.wsConnect.WriteMessage(websocket.TextMessage, buf[:n]) + p.ws.wsMux.Unlock() + } + } + } + } +} + +func (p *ProcessPty) ReadCache(ws *websocket.Conn) { + ws.WriteMessage(websocket.TextMessage, p.cacheBytesBuf.Bytes()) +} + +func (p *ProcessPty) bufHanle(b []byte) { + log := strings.TrimSpace(string(b)) + if utils.RemoveANSI(log) != "" { + p.logReportHandler(log) + } + p.cacheBytesBuf.Write(b) + p.cacheBytesBuf.Next(len(b)) +} + +func (p *ProcessPty) pInit() { + p.SetTermType("pty") + p.Control.changControlChan = make(chan int) + p.stopChan = make(chan struct{}) + p.State.State = 1 + p.Pid = p.cmd.Process.Pid + p.State.startTime = time.Now() + p.cacheBytesBuf = bytes.NewBuffer(make([]byte, config.CF.ProcessMsgCacheBufLimit)) + p.InitPerformanceStatus() + p.initPsutil() + go p.readInit() + go p.monitorHanler() + go p.watchDog() +} + +func RunNewProcessPty(pconfig model.Process) (*ProcessPty, error) { + args := strings.Split(pconfig.Cmd, " ") + cmd := exec.Command(args[0], args[1:]...) // 替换为你要执行的命令及参数 + + processPty := ProcessPty{ + ProcessBase: ProcessBase{ + Name: pconfig.Name, + StartCommand: args, + }, + } + cmd.Dir = pconfig.Cwd + pf, err := pty.Start(cmd) + if err != nil || cmd.Process == nil { + log.Logger.Error("进程启动出错:", err) + return nil, err + } + pty.Setsize(pf, &pty.Winsize{ + Rows: 100, + Cols: 100, + }) + processPty.pty = pf + processPty.cmd = cmd + log.Logger.Infow("创建进程成功") + processPty.setProcessConfig(pconfig) + processPty.pInit() + return &processPty, nil +} diff --git a/service/process/process_std.go b/service/process/process_std.go new file mode 100644 index 0000000..c1f6a51 --- /dev/null +++ b/service/process/process_std.go @@ -0,0 +1,182 @@ +package process + +import ( + "bufio" + "fmt" + "io" + "msm/config" + "msm/log" + "msm/model" + "os/exec" + "strings" + "time" + + "github.com/gorilla/websocket" +) + +type ProcessStd struct { + ProcessBase + cacheLine []string + stdin io.WriteCloser + stdout *bufio.Scanner +} + +func (p *ProcessStd) Kill() error { + return p.cmd.Process.Kill() +} + +func (p *ProcessStd) watchDog() { + state, _ := p.cmd.Process.Wait() + close(p.stopChan) + p.State.State = 0 + if state.ExitCode() != 0 { + log.Logger.Infow("进程停止", "进程名称", p.Name, "exitCode", state.ExitCode(), "进程类型", "std") + p.push(fmt.Sprintf("进程停止,退出码 %d", state.ExitCode())) + if p.Config.AutoRestart { + p.ReStart() + } + } else { + log.Logger.Infow("进程正常退出", "进程名称", p.Name) + p.push("进程正常退出") + } +} + +func (p *ProcessStd) WriteBytes(input []byte) (err error) { + p.logReportHandler(config.CF.ProcessInputPrefix + string(input)) + _, err = p.stdin.Write(append(input, '\n')) + return +} + +func (p *ProcessStd) Write(input string) (err error) { + p.logReportHandler(config.CF.ProcessInputPrefix + input) + _, err = p.stdin.Write([]byte(input + "\n")) + return +} + +func (p *ProcessStd) ReStart() { + if p.State.restartTimes > config.CF.ProcessRestartsLimit { + log.Logger.Warnw("重启次数达到上限", "name", p.Name, "limit", config.CF.ProcessRestartsLimit) + p.State.State = 2 + p.State.Info = "重启次数异常" + p.push("进程重启次数达到上限") + return + } + cmd := exec.Command(p.StartCommand[0], p.StartCommand[1:]...) // 替换为你要执行的命令及参数 + cmd.Dir = p.cmd.Dir + out, err := cmd.StdoutPipe() + if err != nil { + log.Logger.Errorw("重启失败,输出管道获取失败", "err", err) + p.Config.AutoRestart = false + return + } + p.stdout = bufio.NewScanner(out) + p.stdin, err = cmd.StdinPipe() + if err != nil { + log.Logger.Errorw("重启失败,输入管道获取失败", "err", err) + p.Config.AutoRestart = false + return + } + err = cmd.Start() + if err != nil { + log.Logger.Errorw("重启失败,进程启动出错:", "err", err) + p.Config.AutoRestart = false + return + } + p.State.restartTimes++ + log.Logger.Infow("进程启动成功", "进程名称", p.Name, "重启次数", p.State.restartTimes) + p.cmd = cmd + p.pInit() + p.push("进程启动成功") + +} + +func (p *ProcessStd) pInit() { + log.Logger.Infow("创建进程成功") + p.Control.changControlChan = make(chan int) + p.stopChan = make(chan struct{}) + p.State.State = 1 + p.Pid = p.cmd.Process.Pid + p.State.startTime = time.Now() + p.cacheLine = make([]string, config.CF.ProcessMsgCacheLinesLimit) + p.InitPerformanceStatus() + p.initPsutil() + go p.watchDog() + go p.readInit() + go p.monitorHanler() +} + +func (p *ProcessStd) ReadCache(ws *websocket.Conn) { + for _, line := range p.cacheLine { + ws.WriteMessage(websocket.TextMessage, []byte(line)) + } +} + +func (p *ProcessStd) readInit() { + var output string + log.Logger.Debugw("stdout读取线程已启动", "进程名", p.Name, "使用者", p.WhoUsing) + for { + select { + case <-p.stopChan: + { + p.IsUsing.Store(false) + p.WhoUsing = "" + log.Logger.Debugw("stdout读取线程已退出", "进程名", p.Name, "使用者", p.WhoUsing) + return + } + default: + { + output = p.Read() + if p.IsUsing.Load() && output != "" { + p.ws.wsMux.Lock() + p.ws.wsConnect.WriteMessage(websocket.TextMessage, []byte(output)) + p.ws.wsMux.Unlock() + } + } + } + } +} +func (p *ProcessStd) Read() string { + if p.stdout.Scan() { + output := p.stdout.Text() + p.logReportHandler(output) + p.cacheLine = p.cacheLine[1:] + p.cacheLine = append(p.cacheLine, output) + return output + } + return "" +} + +func RunNewProcessStd(pconfig model.Process) (*ProcessStd, error) { + args := strings.Split(pconfig.Cmd, " ") + cmd := exec.Command(args[0], args[1:]...) // 替换为你要执行的命令及参数 + + processStd := ProcessStd{ + ProcessBase: ProcessBase{ + Name: pconfig.Name, + StartCommand: args, + }, + } + cmd.Dir = pconfig.Cwd + out, err := cmd.StdoutPipe() + if err != nil { + log.Logger.Errorw("输出管道获取失败", "err", err) + return nil, err + } + processStd.stdout = bufio.NewScanner(out) + processStd.stdin, err = cmd.StdinPipe() + if err != nil { + log.Logger.Errorw("输入管道获取失败", "err", err) + return nil, err + } + err = cmd.Start() + if err != nil || cmd.Process == nil { + log.Logger.Error("进程启动出错:", err) + return nil, err + } + log.Logger.Infow("创建进程成功", "config", pconfig) + processStd.cmd = cmd + processStd.SetTermType("std") + processStd.pInit() + processStd.setProcessConfig(pconfig) + return &processStd, nil +} diff --git a/service/process/service.go b/service/process/service.go new file mode 100644 index 0000000..86c48d2 --- /dev/null +++ b/service/process/service.go @@ -0,0 +1,124 @@ +package process + +import ( + "errors" + "msm/dao" + "msm/log" + "msm/model" + "strings" + "sync" +) + +type processCtlService struct{} + +var processMap sync.Map = sync.Map{} +var ProcessCtlService = new(processCtlService) + +func (p *processCtlService) AddProcess(uuid int, prcess Process) { + processMap.Store(uuid, prcess) + // processMap.Store("111", prcess) + // return "111" +} + +func (p *processCtlService) KillProcess(uuid int) error { + value, ok := processMap.Load(uuid) + if !ok { + return errors.New("进程不存在") + } + result, ok := value.(Process) + if !ok { + return errors.New("进程类型错误") + } + result.SetAutoRestart(false) + return result.Kill() +} + +func (p *processCtlService) GetProcess(uuid int) (Process, error) { + process, ok := processMap.Load(uuid) + if !ok { + return nil, errors.New("进程获取失败") + + } + result, ok := process.(Process) + if !ok { + return nil, errors.New("进程类型错误") + + } + return result, nil +} + +func (p *processCtlService) KillAllProcess() { + processMap.Range(func(key, value any) bool { + value.(Process).Kill() + return true + }) +} + +func (p *processCtlService) DeleteProcess(uuid int) { + processMap.Delete(uuid) +} + +func (p *processCtlService) GetProcessList() []model.ProcessInfo { + processConfiglist := dao.ProcessDao.GetAllProcessConfig() + return p.getProcessInfoList(processConfiglist) +} + +func (p *processCtlService) GetProcessListByUser(username string) []model.ProcessInfo { + processConfiglist := dao.ProcessDao.GetProcessConfigByUser(username) + return p.getProcessInfoList(processConfiglist) +} + +func (p *processCtlService) getProcessInfoList(processConfiglist []model.Process) []model.ProcessInfo { + processInfoList := []model.ProcessInfo{} + for _, v := range processConfiglist { + pi := model.ProcessInfo{ + Name: v.Name, + Uuid: v.Uuid, + } + if value, ok := processMap.Load(v.Uuid); ok { + process := value.(Process) + pi.State.Info = process.GetStateInfo() + pi.State.State = process.GetStateState() + pi.StartTime = process.GetStartTimeFormat() + pi.User = process.GetWhoUsing() + pi.Usage.Cpu = process.GetCpuUsage() + pi.Usage.Mem = process.GetMemUsage() + pi.Usage.Time = process.GetTimeRecord() + pi.TermType = process.GetTermType() + } + processInfoList = append(processInfoList, pi) + } + return processInfoList +} + +func (p *processCtlService) ProcessInit() { + config := dao.ProcessDao.GetAllProcessConfig() + for _, v := range config { + if !v.AutoRestart { + continue + } + proc, err := RunNewProcess(v) + if err != nil { + log.Logger.Warnw("初始化启动进程失败", v.Name, "name", "err", err) + continue + } + p.AddProcess(v.Uuid, proc) + } +} + +func (p *processCtlService) UpdateProcessConfig(config model.Process) error { + process, ok := processMap.Load(config.Uuid) + if !ok { + return errors.New("进程获取失败") + } + result, ok := process.(Process) + if !ok { + return errors.New("进程类型错误") + } + result.SetConfigLogReport(config.LogReport) + result.SetConfigStatuPush(config.Push) + result.SetConfigAutoRestart(config.AutoRestart) + result.SetStartCommand(strings.Split(config.Cmd, " ")) + result.SetName(config.Name) + return nil +} diff --git a/service/push/push.go b/service/push/push.go new file mode 100644 index 0000000..bb0e55d --- /dev/null +++ b/service/push/push.go @@ -0,0 +1,35 @@ +package push + +import ( + "msm/dao" + "strings" + + "github.com/levigross/grequests" +) + +type pushService struct{} + +var PushService = new(pushService) + +func (p *pushService) Push(placeholders map[string]string) { + pl := dao.PushDao.GetPushList() + for _, v := range pl { + if v.Enable { + if v.Method == "GET" { + grequests.Get(p.getReplaceMessage(placeholders, v.Url), nil) + } + if v.Method == "POST" { + grequests.Post(v.Url, &grequests.RequestOptions{ + JSON: p.getReplaceMessage(placeholders, v.Body), + }) + } + } + } +} + +func (p *pushService) getReplaceMessage(placeholders map[string]string, message string) string { + for k, v := range placeholders { + message = strings.ReplaceAll(message, k, v) + } + return message +} diff --git a/termui/tui.go b/termui/tui.go new file mode 100644 index 0000000..b490ca7 --- /dev/null +++ b/termui/tui.go @@ -0,0 +1,58 @@ +package termui + +// func TermuiInit() { +// // time.Sleep(2 * time.Second) +// err := termbox.Init() +// if err != nil { +// log.Logger.Errorw("termui初始化失败", "err", err) +// return +// } +// homeUi() +// } + +// func homeUi() { +// termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) +// termbox.Flush() +// list := process.ProcessCtlService.GetProcessList() +// fmt.Println() +// for i, v := range list { +// if v.User != "" { +// fmt.Printf(" [%v] %v %v <%v>\n", i, v.Name, v.StartTime, v.User) +// } else { +// fmt.Printf(" [%v] %v %v\n", i, v.Name, v.StartTime) +// } +// } +// input := "" +// fmt.Scan(&input) +// for i, v := range list { +// if input == strconv.Itoa(i) { +// // prcessUi(v.Uuid) +// } +// } +// } + +// func prcessUi(uuid int) { +// termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) +// termbox.Flush() +// proc, err := process.ProcessCtlService.GetProcess(uuid) +// if err != nil { +// log.Logger.Errorw("进程获取失败", "err", err) +// return +// } +// proc.SetControl("") +// go func() { +// for { +// if output := proc.Read(); output != "" { +// fmt.Println(output) +// } +// } +// }() +// go func() { +// input := "" +// for { +// fmt.Scan(&input) +// proc.Write(input + "\n") +// } +// }() + +// } diff --git a/test/test.go b/test/test.go new file mode 100644 index 0000000..78bf89b --- /dev/null +++ b/test/test.go @@ -0,0 +1,111 @@ +package main + +import ( + "io" + "log" + "net/http" + "os/exec" + + "github.com/creack/pty" + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func main() { + http.HandleFunc("/ws", handleWebSocket) + log.Println("Server started at :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("Upgrade:", err) + return + } + defer conn.Close() + + // 启动 Minecraft 服务器 + cmd := exec.Command("java", "-jar", "launcher-airplane.jar") + + // 创建伪终端 + ptmx, err := pty.Start(cmd) + if err != nil { + log.Println("Start pty:", err) + return + } + defer func() { _ = ptmx.Close() }() // 最后关闭伪终端 + + // 创建通道用于读取子程序的输出 + outputChan := make(chan string) + + go func() { + defer close(outputChan) + readOutput(ptmx, outputChan) + }() + + // 将子程序的输出发送到 WebSocket 客户端 + go func() { + for output := range outputChan { + if err := conn.WriteMessage(websocket.TextMessage, []byte(output)); err != nil { + log.Println("WriteMessage:", err) + break + } + } + }() + + // 从 WebSocket 客户端读取消息并发送到子程序的标准输入 + for { + _, message, err := conn.ReadMessage() + if err != nil { + log.Println("ReadMessage:", err) + break + } + // 检查是否是发送 Tab 键的命令 + if string(message) == "SEND_TAB" { + _, err := ptmx.Write([]byte{9}) // Tab 键的 ASCII 码是 9 + if err != nil { + log.Println("Write to stdin:", err) + } + } else if string(message) == "READ_INPUT" { + // 读取伪终端当前输入管道内已经存在的内容 + // 注意:伪终端没有单独的输入缓冲区,输入会立即被处理 + // 这里假设你想获取当前输出内容 + if err := conn.WriteMessage(websocket.TextMessage, []byte("Currently no direct way to fetch unsent input. Consider monitoring the terminal buffer.")); err != nil { + log.Println("WriteMessage:", err) + } + } else { + _, err := ptmx.Write(append(message, '\n')) // 确保命令后有换行符 + if err != nil { + log.Println("Write to stdin:", err) + } + } + } + + // 等待子程序结束 + if err := cmd.Wait(); err != nil { + log.Println("Wait:", err) + return + } +} + +func readOutput(reader io.Reader, outputChan chan<- string) { + buf := make([]byte, 1024) + for { + n, err := reader.Read(buf) + if n > 0 { + outputChan <- string(buf[:n]) + } + if err != nil { + if err != io.EOF { + log.Println("Read:", err) + } + break + } + } +} diff --git a/utils/jwt.go b/utils/jwt.go new file mode 100644 index 0000000..4e06f9f --- /dev/null +++ b/utils/jwt.go @@ -0,0 +1,74 @@ +package utils + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt" +) + +var mySecret = []byte("D9&(#$HI$#Y(FD*A))") + +func keyFunc(_ *jwt.Token) (i interface{}, err error) { + return mySecret, nil +} + +// MyClaims 自定义声明结构体并内嵌 jwt.StandardClaims +// jwt包自带的jwt.StandardClaims只包含了官方字段,若需要额外记录其他字段,就可以自定义结构体 +// 如果想要保存更多信息,都可以添加到这个结构体中 + +type MyClaims struct { + UserName string `json:"user_name"` + jwt.StandardClaims +} + +func GenToken(UserName string) (string, error) { + // 创建一个我们自己的声明的数据 + c := MyClaims{ + UserName, + jwt.StandardClaims{ + ExpiresAt: time.Now().Add( + 240 * time.Hour).Unix(), // 过期时间 + Issuer: "jwt", // 签发人 + }, + } + // 使用指定的签名方法创建签名对象 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) + // 使用指定的secret签名并获得完整的编码后的字符串token + return token.SignedString(mySecret) +} + +// ParseToken 解析JWT +func ParseToken(tokenString string) (*MyClaims, error) { + // 解析token + var mc = new(MyClaims) + token, err := jwt.ParseWithClaims(tokenString, mc, keyFunc) + if err != nil { + return nil, err + } + // 校验token + if token.Valid { + return mc, nil + } + return nil, errors.New("invalid token") +} + +// RefreshToken 刷新AccessToken +func RefreshToken(aToken, rToken string) (newAToken, newRToken string, err error) { + // refresh token无效直接返回 + if _, err = jwt.Parse(rToken, keyFunc); err != nil { + return + } + + // 从旧access token中解析出claims数据 + var claims MyClaims + _, err = jwt.ParseWithClaims(aToken, &claims, keyFunc) + v, _ := err.(*jwt.ValidationError) + + // 当access token是过期错误 并且 refresh token没有过期时就创建一个新的access token + if v.Errors == jwt.ValidationErrorExpired { + token, _ := GenToken(claims.UserName) + return token, "", nil + } + return +} diff --git a/utils/md5.go b/utils/md5.go new file mode 100644 index 0000000..52c108d --- /dev/null +++ b/utils/md5.go @@ -0,0 +1,12 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" +) + +func Md5(str string) string { + h := md5.New() + h.Write([]byte(str)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/utils/unicode.go b/utils/unicode.go new file mode 100644 index 0000000..5d37795 --- /dev/null +++ b/utils/unicode.go @@ -0,0 +1,32 @@ +package utils + +import ( + "regexp" + "unicode/utf8" +) + +func RemoveNotValidUtf8InString(s string) string { + ret := s + if !utf8.ValidString(s) { + v := make([]rune, 0, len(s)) + for i, r := range s { + if r == utf8.RuneError { + _, size := utf8.DecodeRuneInString(s[i:]) + if size == 1 { + continue + } + } + v = append(v, r) + } + ret = string(v) + } + return ret +} + +func RemoveANSI(input string) string { + // Define the regular expression to match ANSI escape sequences + re := regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + // Replace all ANSI escape sequences with an empty string + cleanedString := re.ReplaceAllString(input, "") + return cleanedString +} diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..a052760 --- /dev/null +++ b/utils/utils.go @@ -0,0 +1,8 @@ +package utils + +func Unwarp[T any](result T, err error) T { + if err != nil { + panic(err) + } + return result +}