diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..2ca4329 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,44 @@ +version: 2 +jobs: + build: + docker: + - image: golang:1.20.2-stretch + + steps: + - checkout + + - run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD + + - run: + name: Install GuPM + command: curl -fsSL https://azukaar.github.io/GuPM/install.sh | bash + + - run: + name: Install provider + command: g plugin install https://azukaar.github.io/GuPM-official/repo:provider-go + + - run: + name: Build Linux (ARM) + command: g ci/publish linux arm64 + + - run: + name: Build docker (ARM) + command: g docker arm64 + + - run: + name: Build Linux + command: g ci/publish linux amd64 + + - run: + name: Build docker + command: g docker + +workflows: + version: 2 + build-all: + jobs: + - build: + filters: + branches: + only: + - master \ No newline at end of file diff --git a/.gitignore b/.gitignore index a56b938..7aec21b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ localcert.key dev.json .bin client/dist -config_dev.json \ No newline at end of file +config_dev.json +tests +todo.txt +config_dev.backup.json \ No newline at end of file diff --git a/.gupm_rc.gs b/.gupm_rc.gs index 29efc1e..8977031 100644 --- a/.gupm_rc.gs +++ b/.gupm_rc.gs @@ -1,8 +1,10 @@ // look up dependencies in local go_modules and gupm_modules directories env("GOPATH", run("go", ["env", "GOROOT"]) + ":" + pwd() + "/go_modules" + ":" + pwd() + "/gupm_modules") - +env("GO111MODULE", "off") +env("LOG_LEVEL", "DEBUG") // dev mode env("MONGODB", readJsonFile("dev.json").MONGODB) env("HTTP_PORT", 8080) env("HTTPS_PORT", 8443) -env("CONFIG_FILE", "./config_dev.json") \ No newline at end of file +env("CONFIG_FILE", "./config_dev.json") +env("EZ", "UTC") \ No newline at end of file diff --git a/LICENCE b/LICENCE deleted file mode 100644 index 8410c20..0000000 --- a/LICENCE +++ /dev/null @@ -1,661 +0,0 @@ -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 -. \ No newline at end of file diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000..381512f Binary files /dev/null and b/Logo.png differ diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..c753246 Binary files /dev/null and b/banner.png differ diff --git a/ci/build.gs b/ci/build.gs new file mode 100644 index 0000000..e37d9b6 --- /dev/null +++ b/ci/build.gs @@ -0,0 +1,31 @@ +removeFiles("build") + +var goArgs = ["build", "-o"] + +if(typeof $1 != "undefined" && $1 == "windows") { + goArgs.push("build/cosmos.exe") +} else { + goArgs.push("build/cosmos") +} + +goArgs = goArgs.concat(dir("src/*.go")) + +var archi = "amd64" +if(typeof $2 != "undefined") { + archi = $2 +} + +if(typeof $1 != "undefined" && $1 == "mac") { + env("GOOS", "darwin") + env("GOARCH", archi) + exec("go", goArgs) +} +if(typeof $1 != "undefined" && $1 == "windows") { + env("GOOS", "windows") + env("GOARCH", archi) + exec("go", goArgs) +} else { + env("GOOS", "linux") + env("GOARCH", archi) + exec("go", goArgs) +} diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 38f3861..0000000 --- a/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite App - - -
- - - diff --git a/client/src/main.css b/client/src/main.css deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/main.tsx b/client/src/main.tsx deleted file mode 100644 index 60801e6..0000000 --- a/client/src/main.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import './main.css' - -ReactDOM.createRoot(document.getElementById('root')!).render( - -
Hello
-
-) diff --git a/docker.gs b/docker.gs new file mode 100644 index 0000000..4aa1625 --- /dev/null +++ b/docker.gs @@ -0,0 +1,37 @@ +var version = readJsonFile("gupm.json").version + +var archi = "" +if(typeof $1 != "undefined") { + archi = $1 + version = version + "-" + archi +} + +console.log("Pushing azukaar/cosmos-server:"+version) + +var buildSettings = ["build", "--tag", "azukaar/cosmos-server:"+version] + +if(archi == "arm64") { + buildSettings.push("--platform") + buildSettings.push("linux/arm/v7") + buildSettings.push("--file") + buildSettings.push("./dockerfile.arm64") + buildSettings.push("--tag") + buildSettings.push("azukaar/cosmos-server:latest-arm64") +} else { + buildSettings.push("--tag") + buildSettings.push("azukaar/cosmos-server:latest") +} + +buildSettings.push(".") + +console.log(buildSettings) + +exec("docker", buildSettings) + +exec("docker", ["push", "azukaar/cosmos-server:"+version]) + +if(archi == "arm64") { + exec("docker", ["push", "azukaar/cosmos-server:latest-arm64"]) +} else { + exec("docker", ["push", "azukaar/cosmos-server:latest"]) +} \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..76766a3 --- /dev/null +++ b/dockerfile @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 + +FROM debian + +WORKDIR /app + +COPY build/cosmos . + +VOLUME /config + +EXPOSE 443 80 + +CMD ["./cosmos"] diff --git a/dockerfile.arm64 b/dockerfile.arm64 new file mode 100644 index 0000000..48e1d0d --- /dev/null +++ b/dockerfile.arm64 @@ -0,0 +1,13 @@ +# syntax=docker/dockerfile:1 + +FROM amd64/debian + +WORKDIR /app + +COPY build/cosmos . + +VOLUME /config + +EXPOSE 443 80 + +CMD ["./cosmos"] diff --git a/generate-certificate.sh b/generate-certificate.sh deleted file mode 100644 index ac21cc1..0000000 --- a/generate-certificate.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/ash -FILE_CERT_NAME=localcert -if [ -f "certificates/$FILE_CERT_NAME.crt" ] && [ -f "$FILE_CERT_NAME.key" ] ; then - echo "Cert and Key already exist" -else - echo "Cert and Key does not exist, trying to create new ones..." - apk update && apk add openssl && rm -rf /var/cache/apk/* - openssl req -new -subj "/C=US/ST=California/CN=localhost" \ - -newkey rsa:2048 -nodes -keyout "$FILE_CERT_NAME.key" -out "$FILE_CERT_NAME.csr" - openssl x509 -req -days 365 -in "$FILE_CERT_NAME.csr" -signkey "$FILE_CERT_NAME.key" -out "$FILE_CERT_NAME.crt" -extfile "self-signed-cert.ext" -fi \ No newline at end of file diff --git a/gupm.json b/gupm.json index d44c880..c9394af 100644 --- a/gupm.json +++ b/gupm.json @@ -3,11 +3,15 @@ "cli": { "aliases": { "certificate": "sh generate-certificate.sh", + "client": "g vite dev", "start": "build/bin" } }, "dependencies": { "default": { + "go://github.com/cespare/xxhash/v2": "master", + "go://github.com/go-chi/chi": "master", + "go://github.com/go-chi/httprate": "master", "go://github.com/go-playground/validator/v10": "master", "go://github.com/golang-jwt/jwt": "master", "go://github.com/golang/crypto/": "master", @@ -17,20 +21,12 @@ "go://github.com/lib/pq": "master", "go://github.com/pquerna/ffjson": "master", "go://go.mongodb.org/mongo-driver": "master", - "go://gopkg.in/ffmt.v1": "v1.5.6", - "npm://@esbuild/linux-x64": "0.16.17", - "npm://@vitejs/plugin-react": "3.1.0", - "npm://express": "4.18.2", - "npm://express-ws": "5.0.2", - "npm://react": "18.2.0", - "npm://react-dom": "18.2.0", - "npm://typescript": "4.9.5", - "npm://vite": "4.1.1" + "go://gopkg.in/ffmt.v1": "v1.5.6" }, "defaultProvider": "go" }, - "description": "MyData file server", - "license": "ISC", - "name": "myFileServer", + "description": "Cosmos Server", + "name": "cosmos-server", + "version": "0.0.1", "wrapInstallFolder": "src" } \ No newline at end of file diff --git a/readme.md b/readme.md index 826b5d7..c2a4f9f 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,52 @@ -# GUCO Server +![banner](./banner.png) +# Cosmos Server -blablabla ... +``` +Disclaimer: Cosmos is still in early Alpha stage, please be careful when you use it. It is not (yet, at least ;p) a replacement for proper control and mindfulness of your own security. +``` + +Looking for a **secure** and **robust** way to run your **self-hosted applications**? With **Cosmos**, you can take control of your data and privacy without sacrificing security and stability. + +Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with applications such as **Plex** or **HomeAssistant**, Cosmos is the perfect solution to secure it all. Simply install Cosmos on your server and connect to your applications through it to enjoy built-in security and robustness for all your services, right out of the box. + + * **Authentication** Connect to all your application with the same account, including strong security and multi-factor authentication + * **Automatic HTTPS** certificates provision + * **Anti-bot** protections such as Captcha and IP rate limiting + * **Anti-DDOS** protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting + +And a **lot more planned features** are coming! + +**If you're a self-hosted application developer**, integrate your application with Cosmos and enjoy **secure authentication**, **robust HTTP layer protection**, **HTTPS support**, **user management**, **encryption**, **logging**, **backup**, and more - all with **minimal effort**. And if your users prefer **not to install** Cosmos, your application will **still work seamlessly**. + +# Why use it? + +If you have your own self-hosted data, such as a Plex server, or may be your own photo server, **you expose your data to being hacked, or your server to being highjacked**. + +It is becoming an important **threat to you**. Managing servers, applications and data is **very complex**, and the problem is that **you cannot do it on your own**: how do you know that the photo application's server where you store your family photos has a secure code? + +Because every new self-hosted applications **re-invent the wheel** and implement **crucial parts** such as authentication **from scratch** everytime, the **large majority** of them are very succeptible to be **hacked without too much trouble**. On top of that, you as a user need to make sure you properly control the access to those applciation and keep them updated. + +**Even a major application such as Plex** has been **hacked** in the past, and the data of its users has been exposed. In fact, the recent LastPass happened because a LastPass employee had a Plex server that **wasn't updated to the last version** and was missing an important **security patch**! + +That is the issue Cosmos Server is trying to solve: by providing a secure and robust gateway to all your self-hosted applications, **you can be sure that your data is safe** and that you can access it without having to worry about the security of your applications. # Installation -blablabala ... +Installation is simple using Docker: -# Build locally +``` +docker run -d -p 80:80 -p 443:443 -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest +``` + +you can use `latest-arm64` for arm architecture (ex: NAS or Raspberry) + +You can thing tweak the config file accordingly. Some settings can be changed before end with env var. [see here](https://github.com/azukaar/Cosmos-Server/wiki/Configuration). + +``` + +# How to contribute + +## Setup You need [GuPM](https://github.com/azukaar/GuPM) with the [provider-go](https://github.com/azukaar/GuPM-official#provider-go) plugin to run this project. @@ -14,7 +54,7 @@ You need [GuPM](https://github.com/azukaar/GuPM) with the [provider-go](https:// g make ``` -# Run locally +## Run locally First create a file called dev.json with: @@ -26,5 +66,6 @@ First create a file called dev.json with: ``` g build -g start +g start # this will run server +g client # this will run the client ``` \ No newline at end of file diff --git a/src/config.go b/src/config.go index 7d8de8f..fd3d913 100644 --- a/src/config.go +++ b/src/config.go @@ -1,72 +1,34 @@ package main import ( - "log" "os" "regexp" - "./proxy" "encoding/json" + "./utils" ) -type Config struct { - HTTPConfig HTTPConfig -} - -var defaultConfig = Config{ - HTTPConfig: HTTPConfig{ - TLSCert: "localcert.crt", - TLSKey: "localcert.key", - GenerateMissingTLSCert: true, - HTTPPort: "80", - HTTPSPort: "443", - ProxyConfig: proxy.Config{ - Routes: []proxy.RouteConfig{}, - }, - }, -} - -func GetConfig() Config { - configFile := os.Getenv("CONFIG_FILE") - - if configFile == "" { - configFile = "/cosmos.config.json" - } - - log.Println("Using config file: " + configFile) - - // if file does not exist, create it - if _, err := os.Stat(configFile); os.IsNotExist(err) { - log.Println("Config file does not exist. Creating default config file.") - file, err := os.Create(configFile) - if err != nil { - log.Fatal("[ERROR] Creating Default Config File: " + err.Error()) - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - err = encoder.Encode(defaultConfig) - if err != nil { - log.Fatal("[ERROR] Writing Default Config File: " + err.Error()) - } - - return defaultConfig +func LoadConfig() utils.Config { + configFile := utils.GetConfigFileName() + utils.Log("Using config file: " + configFile) + if utils.CreateDefaultConfigFileIfNecessary() { + utils.LoadBaseMainConfig(utils.DefaultConfig) + return utils.DefaultConfig } file, err := os.Open(configFile) if err != nil { - log.Fatal("[ERROR] Opening Config File: " + err.Error()) + utils.Fatal("Opening Config File: ", err) } defer file.Close() decoder := json.NewDecoder(file) - config := Config{} + config := utils.Config{} err = decoder.Decode(&config) // check file is not empty if err != nil { // check error is not empty if err.Error() == "EOF" { - log.Fatal("[ERROR] Reading Config File: File is empty.") + utils.Fatal("Reading Config File: File is empty.", err) } // get error string @@ -75,8 +37,10 @@ func GetConfig() Config { // replace string in error m1 := regexp.MustCompile(`json: cannot unmarshal ([A-Za-z\.]+) into Go struct field ([A-Za-z\.]+) of type ([A-Za-z\.]+)`) errString = m1.ReplaceAllString(errString, "Invalid JSON in config file.\n > Field $2 is wrong.\n > Type is $1 Should be $3") - log.Fatal("[ERROR] Reading Config File: " + errString) + utils.Fatal("Reading Config File: " + errString, err) } + utils.LoadBaseMainConfig(config) + return config } \ No newline at end of file diff --git a/src/configapi/get.go b/src/configapi/get.go new file mode 100644 index 0000000..ce66b71 --- /dev/null +++ b/src/configapi/get.go @@ -0,0 +1,31 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + "../utils" +) + +func ConfigApiGet(w http.ResponseWriter, req *http.Request) { + if AdminOnly(w, req) != nil { + return + } + + if(req.Method == "GET") { + config := utils.GetBaseMainConfig() + + // delete AuthPrivateKey and TLSKey + config.AuthPrivateKey = "" + config.TLSKey = "" + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": config, + }) + } else { + utils.Error("SettingGet: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} diff --git a/src/configapi/restart.go b/src/configapi/restart.go new file mode 100644 index 0000000..b15f716 --- /dev/null +++ b/src/configapi/restart.go @@ -0,0 +1,26 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + "../utils" +) + +func ConfigApiGet(w http.ResponseWriter, req *http.Request) { + if AdminOnly(w, req) != nil { + return + } + + if(req.Method == "GET") { + utils.RestartServer() + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK" + }) + } else { + utils.Error("Restart: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} diff --git a/src/configapi/set.go b/src/configapi/set.go new file mode 100644 index 0000000..60053fe --- /dev/null +++ b/src/configapi/set.go @@ -0,0 +1,55 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + "../utils" +) + +func ConfigApiSet(w http.ResponseWriter, req *http.Request) { + if AdminOnly(w, req) != nil { + return + } + + if(req.Method == "PUT") { + var request utils.Config + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("SettingsUpdate: Invalid User Request", err1) + utils.HTTPError(w, "User Creation Error", + http.StatusInternalServerError, "UC001") + return + } + + errV := utils.Validate.Struct(request) + if errV != nil { + utils.Error("SettingsUpdate: Invalid User Request", errV) + utils.HTTPError(w, "User Creation Error: " + errV.Error(), + http.StatusInternalServerError, "UC003") + return + } + + // restore AuthPrivateKey and TLSKey + config := utils.GetBaseMainConfig() + request.AuthPrivateKey = config.AuthPrivateKey + request.TLSKey = config.TLSKey + + err := utils.SaveConfigTofile(request) + + if err != nil { + utils.Error("SettingsUpdate: Error saving config to file", err) + utils.HTTPError(w, "Error saving config to file", + http.StatusInternalServerError, "CS001") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK" + }) + } else { + utils.Error("SettingsUpdate: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} diff --git a/src/file/copy.go b/src/file/copy.go deleted file mode 100644 index 39d4907..0000000 --- a/src/file/copy.go +++ /dev/null @@ -1,59 +0,0 @@ -package file - -import ( - "log" - "net/http" - "io" - "encoding/json" - "os" - "../utils" -) - -func copyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, in) - if err != nil { - return err - } - return out.Close() -} - -func FileCopy(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - - fullPath := req.URL.Query().Get("path") - if fullPath == "" { - log.Println("No path specified") - } - - filePath := utils.GetRealPath(fullPath) - - fullDestination := req.URL.Query().Get("destination") - if fullDestination == "" { - log.Println("No destination specified") - } - - destination := utils.GetRealPath(fullDestination) - - // copy file to destination - err := copyFile(filePath, destination) - if err != nil { - log.Fatal(err) - } - - // return json object - json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - }) -} \ No newline at end of file diff --git a/src/file/delete.go b/src/file/delete.go deleted file mode 100644 index b246e9a..0000000 --- a/src/file/delete.go +++ /dev/null @@ -1,31 +0,0 @@ -package file - -import ( - "log" - "net/http" - "encoding/json" - "os" - "../utils" -) - -func FileDelete(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - - fullPath := req.URL.Query().Get("path") - if fullPath == "" { - log.Println("No path specified") - } - - filePath := utils.GetRealPath(fullPath) - - // delete file - err := os.Remove(filePath) - if err != nil { - log.Fatal(err) - } - - // return json object - json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - }) -} \ No newline at end of file diff --git a/src/file/get.go b/src/file/get.go deleted file mode 100644 index 61bb072..0000000 --- a/src/file/get.go +++ /dev/null @@ -1,112 +0,0 @@ -package file - -import ( - "log" - "net/http" - "io" - "strings" - "os" - // "bytes" - // "mime/multipart" - "bufio" - "strconv" - "../utils" -) - -func getExtension(path string) string { - return strings.Split(path, ".")[len(strings.Split(path, ".")) - 1] -} - -func getContentType(path string) string { - switch getExtension(path) { - case "html": - return "text/html" - case "css": - return "text/css" - case "js": - return "application/javascript" - case "png": - return "image/png" - case "jpg": - return "image/jpeg" - case "jpeg": - return "image/jpeg" - case "gif": - return "image/gif" - case "svg": - return "image/svg+xml" - case "mp4": - return "video/mp4" - case "mkv": - return "video/x-matroska" - case "webm": - return "video/webm" - case "mp3": - return "audio/mpeg" - case "wav": - return "audio/wav" - case "ogg": - return "audio/ogg" - default: - return "text/plain" - } -} - -func FileGet(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - - fullPath := req.URL.Query().Get("path") - if fullPath == "" { - log.Println("No path specified") - } - - filePath := utils.GetRealPath(fullPath) - - file, err := os.Open(filePath) - if err != nil { - log.Fatal(err) - } - defer file.Close() - - /*fileBytes, err := ioutil.ReadAll(file) - if err != nil { - log.Fatal(err) - }*/ - - // set header content type depending on file type - w.Header().Set("Content-Type", getContentType(filePath)) - - // multipart file send - if getExtension(filePath) == "mp4" || getExtension(filePath) == "mkv" || getExtension(filePath) == "webm" { - w.Header().Set("Content-Disposition", "attachment; filename=" + filePath) - } - - // open stat file - stat, err := os.Stat(filePath) - - - buffer := bufio.NewReader(file) - - // set content-length - w.Header().Set("Content-Length", strconv.FormatInt(stat.Size(), 10)) - - //copy buffer to client - io.Copy(w, buffer) - - /* - // get file stats - fileStats, err := os.Stat(filePath) - - // return json object with metadata FileStat and content - json.NewEncoder(w).Encode(map[string]interface{}{ - "Metadata": FileStats{ - Name: fileStats.Name(), - Path: filePath, - Size: fileStats.Size(), - Mode: fileStats.Mode(), - ModTime: fileStats.ModTime(), - IsDir: fileStats.IsDir(), - }, - "Content": string(file), - })*/ -} \ No newline at end of file diff --git a/src/file/list.go b/src/file/list.go deleted file mode 100644 index 4af68a2..0000000 --- a/src/file/list.go +++ /dev/null @@ -1,55 +0,0 @@ -package file - -import ( - "log" - "net/http" - "io/ioutil" - "os" - "encoding/json" - "../utils" -) - -func FileList(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - - fullPath := req.URL.Query().Get("path") - if fullPath == "" { - log.Println("No path specified") - } - - filePath := utils.GetRealPath(fullPath) - - files, err := ioutil.ReadDir(filePath) - if err != nil { - log.Fatal(err) - } - - // get folder stats - folderStats, err := os.Stat(filePath) - - // add file FileStats to json object - var fileStats [](utils.FileStats) - for _, file := range files { - fileStats = append(fileStats, utils.FileStats{ - Name: file.Name(), - Path: fullPath + "/" + file.Name(), - Size: file.Size(), - Mode: file.Mode(), - ModTime: file.ModTime(), - IsDir: file.IsDir(), - }) - } - - // return json object - // return json object with metadata FileStat and content - json.NewEncoder(w).Encode(map[string]interface{}{ - "Metadata": utils.FileStats{ - Name: folderStats.Name(), - Size: folderStats.Size(), - Mode: folderStats.Mode(), - ModTime: folderStats.ModTime(), - IsDir: folderStats.IsDir(), - }, - "Content": fileStats, - }) -} \ No newline at end of file diff --git a/src/file/move.go b/src/file/move.go deleted file mode 100644 index 47737fc..0000000 --- a/src/file/move.go +++ /dev/null @@ -1,39 +0,0 @@ -package file - -import ( - "log" - "net/http" - "encoding/json" - "os" - "../utils" -) - -func FileMove(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - - fullPath := req.URL.Query().Get("path") - if fullPath == "" { - log.Println("No path specified") - } - - filePath := utils.GetRealPath(fullPath) - - fullDestination := req.URL.Query().Get("destination") - if fullDestination == "" { - log.Println("No destination specified") - } - - destination := utils.GetRealPath(fullDestination) - - // copy file to destination - - err := os.Rename(filePath, destination) - if err != nil { - log.Fatal(err) - } - - // return json object - json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - }) -} \ No newline at end of file diff --git a/src/httpServer.go b/src/httpServer.go index 8d4d70b..9215fd9 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -3,34 +3,29 @@ package main import ( "net/http" "./utils" - // "./file" - // "./user" + "./user" "./proxy" "github.com/gorilla/mux" - "log" - "os" "strings" + "strconv" + "regexp" + "time" + "encoding/json" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/httprate" + "crypto/tls" ) -type HTTPConfig struct { - TLSCert string - TLSKey string - GenerateMissingTLSCert bool - HTTPPort string - HTTPSPort string - ProxyConfig proxy.Config -} - -var serverPortHTTP = os.Getenv("HTTP_PORT") -var serverPortHTTPS = os.Getenv("HTTPS_PORT") +var serverPortHTTP = "" +var serverPortHTTPS = "" func startHTTPServer(router *mux.Router) { - log.Println("Listening to HTTP on :" + serverPortHTTP) + utils.Log("Listening to HTTP on :" + serverPortHTTP) err := http.ListenAndServe("0.0.0.0:" + serverPortHTTP, router) if err != nil { - log.Fatal(err) + utils.Fatal("Listening to HTTP", err) } } @@ -50,44 +45,146 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) { http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently) })) if err != nil { - log.Fatal(err) + utils.Fatal("Listening to HTTP (Red)", err) } })() - log.Println("Listening to HTTP on :" + serverPortHTTP) - log.Println("Listening to HTTPS on :" + serverPortHTTPS) + utils.Log("Listening to HTTP on :" + serverPortHTTP) + utils.Log("Listening to HTTPS on :" + serverPortHTTPS) + + utils.IsHTTPS = true + + cert, errCert := tls.X509KeyPair(([]byte)(tlsCert), ([]byte)(tlsKey)) + if errCert != nil { + utils.Fatal("Getting Certificate pair", errCert) + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + // Other options + } + + server := http.Server{ + TLSConfig: tlsConfig, + Addr: utils.GetMainConfig().HTTPConfig.Hostname + ":" + serverPortHTTPS, + ReadTimeout: 0, + ReadHeaderTimeout: 10 * time.Second, + WriteTimeout: 0, + IdleTimeout: 30 * time.Second, + Handler: router, + } // start https server - err := http.ListenAndServeTLS("0.0.0.0:" + serverPortHTTPS, tlsCert, tlsKey, router) + err := server.ListenAndServeTLS("", "") if err != nil { - log.Fatal(err) + utils.Fatal("Listening to HTTPS", err) } } +func tokenMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Header.Set("x-cosmos-user", "") + r.Header.Set("x-cosmos-role", "") + + u, err := user.RefreshUserToken(w, r) + + if err != nil { + return + } + + r.Header.Set("x-cosmos-user", u.Nickname) + r.Header.Set("x-cosmos-role", strconv.Itoa((int)(u.Role))) + + // TODO: If external application, remove the cookie from the request + // to prevent leaking, and generate new JWT token + if false { + cookies := r.Header.Get("Cookie") + // This prob dowsnt work + cookieRemoveRegex := regexp.MustCompile(`jwttoken=[^;]*;`) + cookies = cookieRemoveRegex.ReplaceAllString(cookies, "") + r.Header.Set("Cookie", cookies) + + // Replace the token with a application speicfic one + r.Header.Set("x-cosmos-token", "1234567890") + } + + next.ServeHTTP(w, r) + }) +} + +func StartServer() { + baseMainConfig := utils.GetBaseMainConfig() + config := utils.GetMainConfig().HTTPConfig + serverPortHTTP = config.HTTPPort + serverPortHTTPS = config.HTTPSPort -func StartServer(config HTTPConfig) { var tlsCert = config.TLSCert var tlsKey= config.TLSKey - if serverPortHTTP == "" { - serverPortHTTP = config.HTTPPort + configJson, _ := json.MarshalIndent(config, "", " ") + utils.Debug("Configuration" + (string)(configJson)) + + if((tlsCert == "" || tlsKey == "") && config.GenerateMissingTLSCert) { + utils.Log("Generating new TLS certificate") + pub, priv := utils.GenerateRSAWebCertificates() + + baseMainConfig.HTTPConfig.TLSCert = pub + baseMainConfig.HTTPConfig.TLSKey = priv + utils.SetBaseMainConfig(baseMainConfig) + + utils.Log("Saved new TLS certificate") + + tlsCert = pub + tlsKey = priv } - if serverPortHTTPS == "" { - serverPortHTTPS = config.HTTPSPort + if ((config.AuthPublicKey == "" || config.AuthPrivateKey == "") && config.GenerateMissingAuthCert) { + utils.Log("Generating new Auth ED25519 certificate") + pub, priv := utils.GenerateEd25519Certificates() + + baseMainConfig.HTTPConfig.AuthPublicKey = pub + baseMainConfig.HTTPConfig.AuthPrivateKey = priv + utils.SetBaseMainConfig(baseMainConfig) + + utils.Log("Saved new Auth ED25519 certificate") } router := proxy.BuildFromConfig(config.ProxyConfig) - if utils.FileExists(tlsCert) && utils.FileExists(tlsKey) { - log.Println("TLS certificate found, starting HTTPS servers and redirecting HTTP to HTTPS") + router.Use(middleware.Recoverer) + router.Use(middleware.Logger) + router.Use(tokenMiddleware) + router.Use(utils.SetSecurityHeaders) + + srapi := router.PathPrefix("/api").Subrouter() + + srapi.HandleFunc("/login", user.UserLogin) + srapi.HandleFunc("/logout", user.UserLogout) + srapi.HandleFunc("/register", user.UserRegister) + srapi.HandleFunc("/invite", user.UserResendInviteLink) + + srapi.HandleFunc("/users/{nickname}", user.UsersIdRoute) + srapi.HandleFunc("/users", user.UsersRoute) + + srapi.Use(utils.AcceptHeader("application/json")) + srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname)) + srapi.Use(utils.MiddlewareTimeout(5 * time.Second)) + srapi.Use(httprate.Limit(20, 1*time.Minute, + httprate.WithKeyFuncs(httprate.KeyByIP), + httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { + utils.Error("Too many requests. Throttling", nil) + utils.HTTPError(w, "Too many requests", + http.StatusTooManyRequests, "HTTP003") + return + }), + )) + + if tlsCert != "" && tlsKey != "" { + utils.Log("TLS certificate exist, starting HTTPS servers and redirecting HTTP to HTTPS") startHTTPSServer(router, tlsCert, tlsKey) } else { - log.Println("No TLS certificate found, starting HTTP server only") + utils.Log("TLS certificate does not exist, starting HTTP server only") startHTTPServer(router) } } - -func StopServer() { -} \ No newline at end of file diff --git a/src/index.go b/src/index.go index d1c39a3..98d6095 100644 --- a/src/index.go +++ b/src/index.go @@ -1,14 +1,17 @@ package main import ( - "log" + "./utils" + "time" + "math/rand" ) func main() { - log.Println("Starting...") + utils.Log("Starting...") - config := GetConfig() + rand.Seed(time.Now().UnixNano()) - defer StopServer() - StartServer(config.HTTPConfig) + LoadConfig() + + StartServer() } \ No newline at end of file diff --git a/src/proxy/buildFromConfig.go b/src/proxy/buildFromConfig.go index 86af3ff..0020d1d 100644 --- a/src/proxy/buildFromConfig.go +++ b/src/proxy/buildFromConfig.go @@ -3,18 +3,10 @@ package proxy import ( "github.com/gorilla/mux" "net/http" + "../utils" ) -type RouteConfig struct { - Routing Route - Target string -} - -type Config struct { - Routes []RouteConfig -} - -func BuildFromConfig(config Config) *mux.Router { +func BuildFromConfig(config utils.ProxyConfig) *mux.Router { router := mux.NewRouter().StrictSlash(true) router.HandleFunc("/_health", func(w http.ResponseWriter, r *http.Request) { diff --git a/src/proxy/routeTo.go b/src/proxy/routeTo.go index 6dec274..a871977 100644 --- a/src/proxy/routeTo.go +++ b/src/proxy/routeTo.go @@ -4,13 +4,11 @@ import ( "net/http" "net/http/httputil" "net/url" - "log" + "../utils" // "io/ioutil" // "io" // "os" // "golang.org/x/crypto/bcrypt" - - // "../utils" ) // NewProxy takes target host and creates a reverse proxy @@ -24,8 +22,8 @@ func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { // upgrade the request to websocket proxy.ModifyResponse = func(resp *http.Response) error { - log.Println("[INFO] Response from backend: ", resp.Status) - log.Println("[INFO] URL was ", resp.Request.URL) + utils.Debug("Response from backend: " + resp.Status) + utils.Debug("URL was " + resp.Request.URL.String()) return nil } diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go index 936654c..3ef5a0e 100644 --- a/src/proxy/routerGen.go +++ b/src/proxy/routerGen.go @@ -4,23 +4,12 @@ import ( "net/http" "net/http/httputil" "github.com/gorilla/mux" - // "log" - // "io/ioutil" - // "io" - // "os" - // "golang.org/x/crypto/bcrypt" - - // "../utils" + "time" + "../utils" + "github.com/go-chi/httprate" ) -type Route struct { - UseHost bool - Host string - UsePathPrefix bool - PathPrefix string -} - -func RouterGen(route Route, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route { +func RouterGen(route utils.Route, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route { var realDestination http.Handler realDestination = destination @@ -35,7 +24,40 @@ func RouterGen(route Route, router *mux.Router, destination *httputil.ReversePro realDestination = http.StripPrefix(route.PathPrefix, destination) } - origin.Handler(realDestination) + timeout := route.Timeout + + if(timeout == 0) { + timeout = 10000 + } + + throttlePerMinute := route.ThrottlePerMinute + + if(throttlePerMinute == 0) { + throttlePerMinute = 60 + } + + originCORS := route.CORSOrigin + + if originCORS == "" { + if route.UseHost { + originCORS = route.Host + } else { + originCORS = utils.GetMainConfig().HTTPConfig.Hostname + } + } + + origin.Handler( + utils.CORSHeader(originCORS)( + utils.MiddlewareTimeout(timeout * time.Millisecond)( + httprate.Limit(throttlePerMinute, 1*time.Minute, + httprate.WithKeyFuncs(httprate.KeyByIP), + httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { + utils.Error("Too many requests. Throttling", nil) + utils.HTTPError(w, "Too many requests", + http.StatusTooManyRequests, "HTTP003") + return + }), + )(realDestination)))) return origin } \ No newline at end of file diff --git a/src/user/create.go b/src/user/create.go index a1eb50a..f625737 100644 --- a/src/user/create.go +++ b/src/user/create.go @@ -2,7 +2,6 @@ package user import ( "net/http" - "log" // "io" // "os" "encoding/json" @@ -13,52 +12,85 @@ import ( "../utils" ) +type CreateRequestJSON struct { + Nickname string `validate:"required,min=3,max=32,alphanum"` +} + func UserCreate(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) + if AdminOnly(w, req) != nil { + return + } if(req.Method == "POST") { - nickname := req.FormValue("nickname") + var request CreateRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("UserCreation: Invalid User Request", err1) + utils.HTTPError(w, "User Creation Error", + http.StatusInternalServerError, "UC001") + return + } + + errV := utils.Validate.Struct(request) + if errV != nil { + utils.Error("UserCreation: Invalid User Request", errV) + utils.HTTPError(w, "User Creation Error: " + errV.Error(), + http.StatusInternalServerError, "UC003") + return + } + + nickname := utils.Sanitize(request.Nickname) c := utils.GetCollection(utils.GetRootAppId(), "users") user := utils.User{} - err := c.FindOne(nil, map[string]interface{}{ + utils.Debug("UserCreation: Creating user " + nickname) + + err2 := c.FindOne(nil, map[string]interface{}{ "Nickname": nickname, }).Decode(&user) - if err != mongo.ErrNoDocuments { - log.Println("UserCreation: User already exists") - http.Error(w, "User Creation Error", http.StatusNotFound) - } else if err != nil { - log.Println("UserCreation: Error while finding user") - http.Error(w, "User Creation Error", http.StatusInternalServerError) - } else { - + if err2 == mongo.ErrNoDocuments { RegisterKey := utils.GenerateRandomString(24) RegisterKeyExp := time.Now().Add(time.Hour * 24 * 7) - _, err := c.InsertOne(nil, map[string]interface{}{ + _, err3 := c.InsertOne(nil, map[string]interface{}{ "Nickname": nickname, "Password": "", "RegisterKey": RegisterKey, "RegisterKeyExp": RegisterKeyExp, "Role": utils.USER, + "PasswordCycle": 0, + "CreatedAt": time.Now(), }) - if err != nil { - log.Println("UserCreation: Error while creating user") - http.Error(w, "User Creation Error", http.StatusInternalServerError) + if err3 != nil { + utils.Error("UserCreation: Error while creating user", err3) + utils.HTTPError(w, "User Creation Error", + http.StatusInternalServerError, "UC001") + return } json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - "RegisterKey": RegisterKey, - "RegisterKeyExp": RegisterKeyExp, + "status": "OK", + "data": map[string]interface{}{ + "registerKey": RegisterKey, + "registerKeyExp": RegisterKeyExp, + }, }) + } else if err2 == nil { + utils.Error("UserCreation: User already exists", nil) + utils.HTTPError(w, "User already exists", http.StatusConflict, "UC002") + return + } else { + utils.Error("UserCreation: Error while finding user", err2) + utils.HTTPError(w, "User Creation Error", http.StatusInternalServerError, "UC001") + return } } else { - log.Println("UserCreation: Method not allowed" + req.Method) - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + utils.Error("UserCreation: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return } } \ No newline at end of file diff --git a/src/user/delete.go b/src/user/delete.go new file mode 100644 index 0000000..2607ad5 --- /dev/null +++ b/src/user/delete.go @@ -0,0 +1,43 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + + "../utils" +) + +func UserDelete(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + nickname := vars["nickname"] + + if AdminOrItselfOnly(w, req, nickname) != nil { + return + } + + if(req.Method == "DELETE") { + + c := utils.GetCollection(utils.GetRootAppId(), "users") + + utils.Debug("UserDeletion: Deleting user " + nickname) + + _, err := c.DeleteOne(nil, map[string]interface{}{ + "Nickname": nickname, + }) + + if err != nil { + utils.Error("UserDeletion: Error while deleting user", err) + utils.HTTPError(w, "User Deletion Error", http.StatusInternalServerError, "UD001") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("UserDeletion: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/edit.go b/src/user/edit.go new file mode 100644 index 0000000..85403a5 --- /dev/null +++ b/src/user/edit.go @@ -0,0 +1,73 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + "../utils" +) + +type EditRequestJSON struct { + Email string `validate:"email"` +} + +func UserEdit(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + nickname := vars["nickname"] + + if AdminOrItselfOnly(w, req, nickname) != nil { + return + } + + if(req.Method == "PATCH") { + var request EditRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("UserEdit: Invalid User Request", err1) + utils.HTTPError(w, "User Edit Error", http.StatusInternalServerError, "UL001") + return + } + + // Validate request + err2 := utils.Validate.Struct(request) + if err2 != nil { + utils.Error("UserEdit: Invalid User Request", err2) + utils.HTTPError(w, "User request invalid: " + err2.Error(), http.StatusInternalServerError, "UL002") + return + } + + c := utils.GetCollection(utils.GetRootAppId(), "users") + + utils.Debug("UserEdit: Edit user " + nickname) + + toSet := map[string]interface{}{} + if request.Email != "" { + + if AdminOnly(w, req) != nil { + return + } + + toSet["Email"] = request.Email + } + + _, err := c.UpdateOne(nil, map[string]interface{}{ + "Nickname": nickname, + }, map[string]interface{}{ + "$set": toSet, + }) + + if err != nil { + utils.Error("UserEdit: Error while getting user", err) + utils.HTTPError(w, "User Edit Error", http.StatusInternalServerError, "UE001") + return + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("UserEdit: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/get.go b/src/user/get.go new file mode 100644 index 0000000..1a2fa35 --- /dev/null +++ b/src/user/get.go @@ -0,0 +1,47 @@ +package user + +import ( + "net/http" + "encoding/json" + "github.com/gorilla/mux" + "../utils" +) + +func UserGet(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + nickname := utils.Sanitize(vars["nickname"]) + + if AdminOrItselfOnly(w, req, nickname) != nil { + return + } + + if(req.Method == "GET") { + + c := utils.GetCollection(utils.GetRootAppId(), "users") + + utils.Debug("UserGet: Get user " + nickname) + + user := utils.User{} + + err := c.FindOne(nil, map[string]interface{}{ + "Nickname": nickname, + }).Decode(&user) + + if err != nil { + utils.Error("UserGet: Error while getting user", err) + utils.HTTPError(w, "User Get Error", http.StatusInternalServerError, "UD001") + return + } + + user.Link = "/api/user/" + user.Nickname + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": user, + }) + } else { + utils.Error("UserGet: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/list.go b/src/user/list.go new file mode 100644 index 0000000..7ab3410 --- /dev/null +++ b/src/user/list.go @@ -0,0 +1,79 @@ +package user + +import ( + "net/http" + "encoding/json" + "../utils" + "go.mongodb.org/mongo-driver/mongo/options" + "strconv" + "math" +) + +var maxLimit = 100 + +func UserList(w http.ResponseWriter, req *http.Request) { + if AdminOnly(w, req) != nil { + return + } + + limit, _ := strconv.Atoi(req.URL.Query().Get("limit")) + // from, _ := req.URL.Query().Get("from") + + if limit == 0 { + limit = maxLimit + } + + if(req.Method == "GET") { + c := utils.GetCollection(utils.GetRootAppId(), "users") + + utils.Debug("UserList: List user ") + + userList := []utils.User{} + + l := int64(math.Max((float64)(maxLimit), (float64)(limit))) + + fOpt := options.FindOptions{ + Limit: &l, + } + + // TODO: Implement pagination + + cursor, errDB := c.Find( + nil, + map[string]interface{}{ + // "_id": map[string]interface{}{ + // "$gt": from, + // }, + }, + &fOpt, + ) + + if errDB != nil { + utils.Error("UserList: Error while getting user", errDB) + utils.HTTPError(w, "User Get Error", http.StatusInternalServerError, "UL001") + return + } + + for cursor.Next(nil) { + user := utils.User{} + errDec := cursor.Decode(&user) + if errDec != nil { + utils.Error("UserList: Error while decoding user", errDec) + utils.HTTPError(w, "User Get Error", http.StatusInternalServerError, "UL001") + return + } + user.Link = "/api/user/" + user.Nickname + userList = append(userList, user) + } + + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": userList, + }) + } else { + utils.Error("UserList: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/login.go b/src/user/login.go index 9b1378c..cb2b4f9 100644 --- a/src/user/login.go +++ b/src/user/login.go @@ -2,76 +2,77 @@ package user import ( "net/http" - "log" "math/rand" "encoding/json" "go.mongodb.org/mongo-driver/mongo" - "time" "golang.org/x/crypto/bcrypt" - "github.com/golang-jwt/jwt" + "time" "../utils" ) -func UserLogin(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) +type LoginRequestJSON struct { + Nickname string `validate:"required,min=3,max=32,alphanum"` + Password string `validate:"required,min=8,max=128,containsany=!@#$%^&*()_+,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"` +} +func UserLogin(w http.ResponseWriter, req *http.Request) { if(req.Method == "POST") { time.Sleep(time.Duration(rand.Float64()*2)*time.Second) - nickname := req.FormValue("nickname") - password := req.FormValue("password") - - err := bcrypt.CompareHashAndPassword([]byte(utils.GetHash()), []byte(password)) - - if err != nil { - log.Println("UserLogin: Encryption error") - http.Error(w, "User Logging Error", http.StatusUnauthorized) + var request LoginRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("UserLogin: Invalid User Request", err1) + utils.HTTPError(w, "User Login Error", http.StatusInternalServerError, "UL001") + return } c := utils.GetCollection(utils.GetRootAppId(), "users") - err = c.FindOne(nil, map[string]interface{}{ + nickname := utils.Sanitize(request.Nickname) + password := request.Password + + user := utils.User{} + + utils.Debug("UserLogin: Logging user " + nickname) + + err3 := c.FindOne(nil, map[string]interface{}{ "Nickname": nickname, - "Password": password, - }).Decode(&utils.User{}) + }).Decode(&user) - if err == mongo.ErrNoDocuments { - log.Println("UserLogin: User not found") - http.Error(w, "User Logging Error", http.StatusNotFound) - } else if err != nil { - log.Println("UserLogin: Error while finding user") - http.Error(w, "User Logging Error", http.StatusInternalServerError) + if err3 == mongo.ErrNoDocuments { + bcrypt.CompareHashAndPassword([]byte("$2a$14$4nzsVwEnR3.jEbMTME7kqeCo4gMgR/Tuk7ivNExvXjr73nKvLgHka"), []byte("dummyPassword")) + utils.Error("UserLogin: User not found", err3) + utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001") + return + } else if err3 != nil { + bcrypt.CompareHashAndPassword([]byte("$2a$14$4nzsVwEnR3.jEbMTME7kqeCo4gMgR/Tuk7ivNExvXjr73nKvLgHka"), []byte("dummyPassword")) + utils.Error("UserLogin: Error while finding user", err3) + utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001") + return + } else if user.Password == "" { + utils.Error("UserLogin: User not registered", nil) + utils.HTTPError(w, "User not registered", http.StatusUnauthorized, "UL002") + return } else { - token := jwt.New(jwt.SigningMethodEdDSA) - claims := token.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(30 * 24 * time.Hour) - claims["authorized"] = true - claims["nickname"] = nickname - - tokenString, err := token.SignedString(utils.GetPrivateAuthKey()) - - if err != nil { - log.Println("UserLogin: Error while signing token") - http.Error(w, "User Logging Error", http.StatusInternalServerError) + err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + + if err2 != nil { + utils.Error("UserLogin: Encryption error", err2) + utils.HTTPError(w, "User Logging Error", http.StatusUnauthorized, "UL001") + return } - expiration := time.Now().Add(30 * 24 * time.Hour) + SendUserToken(w, user) - cookie := http.Cookie{ - Name: "jwttoken", - Value: tokenString, - Expires: expiration, - } - - http.SetCookie(w, &cookie) + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) } - - json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - }) } else { - log.Println("UserLogin: Method not allowed" + req.Method) - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + utils.Error("UserLogin: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return } } diff --git a/src/user/logout.go b/src/user/logout.go new file mode 100644 index 0000000..7d52516 --- /dev/null +++ b/src/user/logout.go @@ -0,0 +1,23 @@ +package user + +import ( + "net/http" + "encoding/json" + "../utils" +) + +func UserLogout(w http.ResponseWriter, req *http.Request) { + if(req.Method == "GET") { + utils.Debug("UserLogout: Logging out user") + + logOutUser(w); + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + }) + } else { + utils.Error("UserLogin: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/user/register.go b/src/user/register.go index b399a8d..4d7ab26 100644 --- a/src/user/register.go +++ b/src/user/register.go @@ -2,7 +2,6 @@ package user import ( "net/http" - "log" "math/rand" "encoding/json" "go.mongodb.org/mongo-driver/mongo" @@ -12,64 +11,94 @@ import ( "../utils" ) -func UserRegister(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) +type RegisterRequestJSON struct { + Nickname string `validate:"required,min=3,max=32,alphanum"` + Password string `validate:"required,min=8,max=128,containsany=!@#$%^&*()_+,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"` + RegisterKey string `validate:"required,min=1,max=512,alphanum"` +} +func UserRegister(w http.ResponseWriter, req *http.Request) { if(req.Method == "POST") { time.Sleep(time.Duration(rand.Float64()*2)*time.Second) + + var request RegisterRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("UserRegister: Invalid User Request", err1) + utils.HTTPError(w, "User Register Error", http.StatusInternalServerError, "UR001") + return + } - nickname := req.FormValue("nickname") - registerKey := req.FormValue("registerKey") - password := req.FormValue("password") + errV := utils.Validate.Struct(request) + if errV != nil { + utils.Error("UserRegister: Invalid User Request", errV) + utils.HTTPError(w, "User Register Error: " + errV.Error(), http.StatusInternalServerError, "UR002") + return + } - err := bcrypt.CompareHashAndPassword([]byte(utils.GetHash()), []byte(password)) + nickname := utils.Sanitize(request.Nickname) + password := request.Password + registerKey := request.RegisterKey - if err != nil { - log.Println("UserRegister: Encryption error") - http.Error(w, "User Register Error", http.StatusUnauthorized) + utils.Debug("UserRegister: Registering user " + nickname) + + hashedPassword, err2 := bcrypt.GenerateFromPassword([]byte(password), 14) + + if err2 != nil { + utils.Error("UserRegister: Encryption error", err2) + utils.HTTPError(w, "User Register Error", http.StatusUnauthorized, "UR001") + return } c := utils.GetCollection(utils.GetRootAppId(), "users") user := utils.User{} - err = c.FindOne(nil, map[string]interface{}{ + err3 := c.FindOne(nil, map[string]interface{}{ "Nickname": nickname, "RegisterKey": registerKey, - "Password": "", }).Decode(&user) - if err == mongo.ErrNoDocuments { - log.Println("UserRegister: User not found") - http.Error(w, "User Register Error", http.StatusNotFound) - } else if !user.RegisterKeyExp.Before(time.Now()) { - log.Println("UserRegister: Link expired") - http.Error(w, "User Register Error", http.StatusNotFound) - } else if err != nil { - log.Println("UserRegister: Error while finding user") - http.Error(w, "User Register Error", http.StatusInternalServerError) + if err3 == mongo.ErrNoDocuments { + utils.Error("UserRegister: User not found", err3) + utils.HTTPError(w, "User Register Error", http.StatusInternalServerError, "UR001") + return + } else if err3 != nil { + utils.Error("UserRegister: Error while finding user", err3) + utils.HTTPError(w, "User Register Error", http.StatusInternalServerError, "UR001") + return + } else if user.RegisterKeyExp.Before(time.Now()) { + utils.Error("UserRegister: Link expired", nil) + utils.HTTPError(w, "User Register Error", http.StatusInternalServerError, "UR001") + return } else { - _, err := c.UpdateOne(nil, map[string]interface{}{ + _, err4 := c.UpdateOne(nil, map[string]interface{}{ "Nickname": nickname, "RegisterKey": registerKey, "Password": "", }, map[string]interface{}{ - "Password": password, - "RegisterKey": "", - "RegisterKeyExp": time.Time{}, + "$set": map[string]interface{}{ + "Password": hashedPassword, + "RegisterKey": "", + "RegisterKeyExp": time.Time{}, + "RegisteredAt": time.Now(), + "PassowrdCycle": user.PasswordCycle + 1, + }, }) - if err != nil { - log.Println("UserRegister: Error while updating user") - http.Error(w, "User Register Error", http.StatusInternalServerError) + if err4 != nil { + utils.Error("UserRegister: Error while updating user", err4) + utils.HTTPError(w, "User Register Error", http.StatusInternalServerError, "UR001") + return } } json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", + "status": "OK", }) } else { - log.Println("UserRegister: Method not allowed" + req.Method) - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + utils.Error("UserRegister: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return } } \ No newline at end of file diff --git a/src/user/resend.go b/src/user/resend.go index 17b3ef3..2b9dede 100644 --- a/src/user/resend.go +++ b/src/user/resend.go @@ -2,7 +2,6 @@ package user import ( "net/http" - "log" "encoding/json" "go.mongodb.org/mongo-driver/mongo" "time" @@ -10,50 +9,78 @@ import ( "../utils" ) +type InviteRequestJSON struct { + Nickname string `validate:"required,min=3,max=32,alphanum"` +} + func UserResendInviteLink(w http.ResponseWriter, req *http.Request) { - utils.SetHeaders(w) - if(req.Method == "POST") { - id := req.FormValue("id") + var request InviteRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("UserInvite: Invalid User Request", err1) + utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "US001") + return + } + nickname := utils.Sanitize(request.Nickname) + + if AdminOrItselfOnly(w, req, nickname) != nil { + return + } + + utils.Debug("Re-Sending an invite to " + nickname) + c := utils.GetCollection(utils.GetRootAppId(), "users") user := utils.User{} + // TODO: If not logged in as Admin, check email too + err := c.FindOne(nil, map[string]interface{}{ - "_id": id, + "Nickname": nickname, }).Decode(&user) if err == mongo.ErrNoDocuments { - log.Println("UserResend: User not found") - http.Error(w, "User Resend Invite Error", http.StatusNotFound) + utils.Error("UserInvite: User not found", err) + utils.HTTPError(w, "User Send Invite Error", http.StatusNotFound, "US001") + return } else if err != nil { - log.Println("UserResend: Error while finding user") - http.Error(w, "User Resend Invite Error", http.StatusInternalServerError) + utils.Error("UserInvite: Error while finding user", err) + utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "US001") + return } else { RegisterKeyExp := time.Now().Add(time.Hour * 24 * 7) + RegisterKey := utils.GenerateRandomString(24) _, err := c.UpdateOne(nil, map[string]interface{}{ - "_id": id, + "nickname": nickname, }, map[string]interface{}{ "$set": map[string]interface{}{ "RegisterKeyExp": RegisterKeyExp, + "RegisterKey": RegisterKey, }, }) if err != nil { - log.Println("UserResend: Error while updating user") - http.Error(w, "User Resend Invite Error", http.StatusInternalServerError) + utils.Error("UserInvite: Error while updating user", err) + utils.HTTPError(w, "User Send Invite Error", http.StatusInternalServerError, "US001") + return } + // TODO: Only send registerKey if logged in already + json.NewEncoder(w).Encode(map[string]interface{}{ - "Status": "OK", - "RegisterKey": user.RegisterKey, - "RegisterKeyExp": RegisterKeyExp, + "status": "OK", + "data": map[string]interface{}{ + "registerKey": user.RegisterKey, + "registerKeyExp": RegisterKeyExp, + }, }) } } else { - log.Println("UserResend: Method not allowed" + req.Method) - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + utils.Error("UserInvite: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return } } \ No newline at end of file diff --git a/src/user/token.go b/src/user/token.go new file mode 100644 index 0000000..3697f1e --- /dev/null +++ b/src/user/token.go @@ -0,0 +1,215 @@ +package user + +import ( + "net/http" + "../utils" + "github.com/golang-jwt/jwt" + "errors" + "strings" + "time" + "strconv" +) + +func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) { + cookie, err := req.Cookie("jwttoken") + + if err != nil { + return utils.User{}, nil + } + + tokenString := cookie.Value + + if tokenString == "" { + return utils.User{}, nil + } + + ed25519Key, errK := jwt.ParseEdPublicKeyFromPEM([]byte(utils.GetPublicAuthKey())) + + if errK != nil { + utils.Error("UserToken: Cannot read auth public key", errK) + utils.HTTPError(w, "Authorization Error", http.StatusInternalServerError, "A001") + return utils.User{}, errors.New("Cannot read auth public key") + } + + parts := strings.Split(tokenString, ".") + + errT := jwt.SigningMethodEdDSA.Verify(strings.Join(parts[0:2], "."), parts[2], ed25519Key) + + if errT != nil { + utils.Error("UserToken: Token likely falsified", errT) + logOutUser(w) + redirectToReLogin(w, req) + return utils.User{}, errors.New("Token likely falsified") + } + + type claimsType struct { + nickname string + } + + claims := jwt.MapClaims{} + + _, errP := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return ed25519Key, nil + }) + + if errP != nil { + utils.Error("UserToken: token is not valid", nil) + logOutUser(w) + redirectToReLogin(w, req) + return utils.User{}, errors.New("Token not valid") + } + + nickname := claims["nickname"].(string) + passwordCycle := int(claims["passwordCycle"].(float64)) + + userInBase := utils.User{} + + c := utils.GetCollection(utils.GetRootAppId(), "users") + + errDB := c.FindOne(nil, map[string]interface{}{ + "Nickname": nickname, + }).Decode(&userInBase) + + if errDB != nil { + utils.Error("UserToken: User not found", errDB) + logOutUser(w) + redirectToReLogin(w, req) + return utils.User{}, errors.New("User not found") + } + + if userInBase.PasswordCycle != passwordCycle { + utils.Error("UserToken: Password cycle changed, token is too old", nil) + logOutUser(w) + redirectToReLogin(w, req) + return utils.User{}, errors.New("Password cycle changed, token is too old") + } + + return userInBase, nil +} + +func GetUserR(req *http.Request) (string, string) { + return req.Header.Get("x-cosmos-user"), req.Header.Get("x-cosmos-role") +} + +func logOutUser(w http.ResponseWriter) { + cookie := http.Cookie{ + Name: "jwttoken", + Value: "", + Expires: time.Now().Add(-time.Hour * 24 * 365), + } + + http.SetCookie(w, &cookie) + + // TODO: Remove all other cookies from apps + + // TODO: logout every other device if asked by increasing passwordcycle +} + +func redirectToReLogin(w http.ResponseWriter, req *http.Request) { + http.Redirect(w, req, "/login?invalid=1&redirect=" + req.URL.Path, http.StatusFound) +} + +func SendUserToken(w http.ResponseWriter, user utils.User) { + expiration := time.Now().Add(2 * 24 * time.Hour) + + token := jwt.New(jwt.SigningMethodEdDSA) + claims := token.Claims.(jwt.MapClaims) + claims["exp"] = expiration.Unix() + claims["role"] = user.Role + claims["nickname"] = user.Nickname + claims["passwordCycle"] = user.PasswordCycle + claims["iat"] = time.Now().Unix() + claims["nbf"] = time.Now().Unix() + + key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey())) + + if err5 != nil { + utils.Error("UserLogin: Error while retrieving signing key", err5) + utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001") + return + } + + tokenString, err4 := token.SignedString(key) + + if err4 != nil { + utils.Error("UserLogin: Error while signing token", err4) + utils.HTTPError(w, "User Logging Error", http.StatusInternalServerError, "UL001") + return + } + + + cookie := http.Cookie{ + Name: "jwttoken", + Value: tokenString, + Expires: expiration, + HttpOnly: true, + // TODO: high level cookie for SSO + // Should re-generate app specific cookies on subdomains + // Domain: "yoursite.com", + } + // cookie2 := http.Cookie{ + // Name: "dummy", + // Value: "asdasdadsasd", + // Expires: expiration, + // HttpOnly: true, + // } + + http.SetCookie(w, &cookie) + // http.SetCookie(w, &cookie2) +} + +func loggedInOnly(w http.ResponseWriter, req *http.Request) error { + userNickname := req.Header.Get("x-cosmos-user") + role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role")) + isUserLoggedIn := role > 0 + + if !isUserLoggedIn || userNickname == "" { + utils.Error("LoggedInOnly: User is not logged in", nil) + http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + return errors.New("User not logged in") + } + + return nil +} + +func AdminOnly(w http.ResponseWriter, req *http.Request) error { + userNickname := req.Header.Get("x-cosmos-user") + role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role")) + isUserLoggedIn := role > 0 + isUserAdmin := role > 1 + + if !isUserLoggedIn || userNickname == "" { + utils.Error("AdminOnly: User is not logged in", nil) + http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + return errors.New("User not logged in") + } + + if isUserLoggedIn && !isUserAdmin { + utils.Error("AdminOnly: User is not admin", nil) + utils.HTTPError(w, "Unauthorized", http.StatusUnauthorized, "HTTP002") + return errors.New("User not Admin") + } + + return nil +} + +func AdminOrItselfOnly(w http.ResponseWriter, req *http.Request, nickname string) error { + userNickname := req.Header.Get("x-cosmos-user") + role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role")) + isUserLoggedIn := role > 0 + isUserAdmin := role > 1 + + if !isUserLoggedIn || userNickname == "" { + utils.Error("AdminOrItselfOnly: User is not logged in", nil) + http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound) + return errors.New("User not logged in") + } + + if nickname != userNickname && !isUserAdmin { + utils.Error("AdminOrItselfOnly: User is not admin", nil) + utils.HTTPError(w, "Unauthorized", http.StatusUnauthorized, "HTTP002") + return errors.New("User not Admin") + } + + return nil +} \ No newline at end of file diff --git a/src/user/userRoute.go b/src/user/userRoute.go new file mode 100644 index 0000000..f679f10 --- /dev/null +++ b/src/user/userRoute.go @@ -0,0 +1,32 @@ +package user + +import ( + "net/http" + "../utils" +) + +func UsersIdRoute(w http.ResponseWriter, req *http.Request) { + if(req.Method == "DELETE") { + UserDelete(w, req) + } else if (req.Method == "GET") { + UserGet(w, req) + } else if (req.Method == "PATCH") { + UserEdit(w, req) + } else { + utils.Error("UserRoute: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} + +func UsersRoute(w http.ResponseWriter, req *http.Request) { + if (req.Method == "POST") { + UserCreate(w, req) + } else if (req.Method == "GET") { + UserList(w, req) + } else { + utils.Error("UserRoute: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/utils/certificates.go b/src/utils/certificates.go new file mode 100644 index 0000000..665a1c4 --- /dev/null +++ b/src/utils/certificates.go @@ -0,0 +1,108 @@ +package utils + +import ( + "crypto/x509" + "encoding/pem" + "math/big" + "time" + "crypto/rand" + "crypto/rsa" + "crypto/ed25519" + "crypto/x509/pkix" + "encoding/asn1" +) + +func GenerateRSAWebCertificates() (string, string) { + // generate self signed certificate + + // generate private key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + Fatal("Generating RSA Key", err) + } + + // generate public key + publicKey := &privateKey.PublicKey + + // random SerialNumber + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + Fatal("Generating Serial Number", err) + } + + // generate certificate + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Cosmos Personal Server"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + + DNSNames: []string{GetMainConfig().HTTPConfig.Hostname}, + + // IPAddresses: []net.IP{}, + + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + + AuthorityKeyId: []byte{1, 2, 3, 4, 5}, + + PermittedDNSDomainsCritical: false, + + PermittedDNSDomains: []string{GetMainConfig().HTTPConfig.Hostname}, + + // PermittedIPRanges: , + + ExcludedDNSDomains: []string{}, + + // ExcludedIPRanges:, + + PermittedURIDomains: []string{}, + + ExcludedURIDomains: []string{}, + + CRLDistributionPoints: []string{}, + + PolicyIdentifiers: []asn1.ObjectIdentifier{}, + + } + + // create certificate + + cert, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey, privateKey) + if err != nil { + Fatal("Creating RSA Key", err) + } + + // return private , and public key + + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})), string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})) +} + +func GenerateEd25519Certificates() (string, string) { + // generate self signed certificate + + // generate private key + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + Fatal("Generating ed25519 Key", err) + } + + bpriv, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + Fatal("Generating ed25519 private Key", err) + } + + bpub, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + Fatal("Generating ed25519 public Key", err) + } + + return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: bpub})), string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: bpriv})) +} \ No newline at end of file diff --git a/src/utils/config.go b/src/utils/config.go deleted file mode 100644 index 56accff..0000000 --- a/src/utils/config.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "context" - "log" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo/options" - - "github.com/imdario/mergo" -) - -type GucoConfiguration struct { - Test string - Caca string -} - -var defaultConfig = GucoConfiguration{ - Test: "test", - Caca: "prout", -} - -func GetConfigs() GucoConfiguration { - c := GetCollection(GetRootAppId(), "Configurations") - config := GucoConfiguration{} - err := c.FindOne(context.TODO(), bson.M{"_id": GetRootAppId()}).Decode(&config) - if err == mongo.ErrNoDocuments { - log.Println("Record does not exist") - } else if err != nil { - log.Fatal(err) - } - - mergo.Merge(&config, defaultConfig) - - return config; -} - -func SetConfig(config GucoConfiguration) { - currentConfig := GetConfigs() - - mergo.Merge(&config, currentConfig) - - c := GetCollection(GetRootAppId(), "Configurations") - - opts := options.Update().SetUpsert(true) - filter := bson.D{{"_id", GetRootAppId()}} - update := bson.D{{"$set", config}} - - _, err := c.UpdateOne(context.Background(), filter, update, opts) - if err != nil { - log.Fatal(err) - } -} \ No newline at end of file diff --git a/src/utils/db.go b/src/utils/db.go index ec6a585..555cbe1 100644 --- a/src/utils/db.go +++ b/src/utils/db.go @@ -2,7 +2,6 @@ package utils import ( "context" - "log" "os" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" @@ -13,7 +12,7 @@ import ( var client *mongo.Client func DB() { - log.Println("Connecting to the database...") + Log("Connecting to the database...") uri := os.Getenv("MONGODB") + "/?retryWrites=true&w=majority" @@ -21,31 +20,38 @@ func DB() { client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri)) if err != nil { - log.Fatal(err) + Fatal("DB", err) } defer func() { }() // Ping the primary if err := client.Ping(context.TODO(), readpref.Primary()); err != nil { - log.Fatal(err) + Fatal("DB", err) } - log.Println("Successfully connected to the database.") + Log("Successfully connected to the database.") } func Disconnect() { if err := client.Disconnect(context.TODO()); err != nil { - log.Fatal(err) + Fatal("DB", err) } } func GetCollection(applicationId string, collection string) *mongo.Collection { - name := os.Getenv("MONGODB_NAME"); if name == "" { - name = "GUCO" + if client == nil { + DB() } - log.Println("Getting collection " + applicationId + "_" + collection + " from database " + name) + + name := os.Getenv("MONGODB_NAME"); if name == "" { + name = "COSMOS" + } + + Debug("Getting collection " + applicationId + "_" + collection + " from database " + name) + c := client.Database(name).Collection(applicationId + "_" + collection) + return c } diff --git a/src/utils/log.go b/src/utils/log.go new file mode 100644 index 0000000..b5cd2c9 --- /dev/null +++ b/src/utils/log.go @@ -0,0 +1,50 @@ +package utils + +import ( + "log" +) + +var Reset = "\033[0m" +var Red = "\033[31m" +var Green = "\033[32m" +var Yellow = "\033[33m" +var Blue = "\033[34m" +var Purple = "\033[35m" +var Cyan = "\033[36m" +var Gray = "\033[37m" +var White = "\033[97m" + +func Debug(message string) { + ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + if ll <= DEBUG { + log.Println(Purple + "[DEBUG] " + message + Reset) + } +} + +func Log(message string) { + ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + if ll <= INFO { + log.Println(Blue + "[INFO] " + message + Reset) + } +} + +func Warn(message string) { + ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + if ll <= WARNING { + log.Println(Yellow + "[WARN] " + message + Reset) + } +} + +func Error(message string, err error) { + ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + if ll <= ERROR { + log.Println(Red + "[ERROR] " + message + " : " + err.Error() + Reset) + } +} + +func Fatal(message string, err error) { + ll := LoggingLevelLabels[GetMainConfig().LoggingLevel] + if ll <= ERROR { + log.Fatal(Red + "[FATAL] " + message + " : " + err.Error() + Reset) + } +} diff --git a/src/utils/middleware.go b/src/utils/middleware.go new file mode 100644 index 0000000..5a1a04e --- /dev/null +++ b/src/utils/middleware.go @@ -0,0 +1,69 @@ +package utils + +import ( + "context" + "net/http" + "time" +) + +// https://github.com/go-chi/chi/blob/master/middleware/timeout.go + +func MiddlewareTimeout(timeout time.Duration) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), timeout) + defer func() { + cancel() + if ctx.Err() == context.DeadlineExceeded { + Error("Request Timeout. Cancelling.", ctx.Err()) + HTTPError(w, "Gateway Timeout", + http.StatusGatewayTimeout, "HTTP002") + return + } + }() + + r = r.WithContext(ctx) + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} + +func SetSecurityHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if(IsHTTPS) { + // TODO: Add preload if we have a valid certificate + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + } + + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + // w.Header().Set("Referrer-Policy", "no-referrer") + + next.ServeHTTP(w, r) + }) +} + +func CORSHeader(origin string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + w.Header().Set("Access-Control-Allow-Credentials", "true") + + next.ServeHTTP(w, r) + }) + } +} + +func AcceptHeader(accept string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", accept) + + next.ServeHTTP(w, r) + }) + } +} diff --git a/src/utils/types.go b/src/utils/types.go index 7605c7b..71f76ed 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -3,29 +3,86 @@ package utils import ( "os" "time" + "go.mongodb.org/mongo-driver/bson/primitive" ) -type Role string +type Role int +type LoggingLevel string const ( - GUEST string = "GUEST" - USER = "USER" - ADMIN = "ADMIN" + GUEST = 0 + USER = 1 + ADMIN = 2 ) +const ( + DEBUG = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 +) + +var LoggingLevelLabels = map[LoggingLevel]int{ + "DEBUG": DEBUG, + "INFO": INFO, + "WARNING": WARNING, + "ERROR": ERROR, +} + type FileStats struct { - Name string - Path string - Size int64 - Mode os.FileMode - ModTime time.Time - IsDir bool + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime time.Time `json:"modTime"` + IsDir bool `json:"isDir"` } type User struct { - Nickname string `validate:"required"` - Password string `validate:"required"` - RegisterKey string - RegisterKeyExp time.Time - Role Role `validate:"required"` + ID primitive.ObjectID `json:"-" bson:"_id,omitempty"` + Nickname string `validate:"required" json:"nickname"` + Password string `validate:"required" json:"-"` + RegisterKey string `json:"registerKey"` + RegisterKeyExp time.Time `json:"registerKeyExp"` + Role Role `validate:"required" json:"role"` + PasswordCycle int `json:"-"` + Link string `json:"link"` +} + +type Config struct { + LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"` + HTTPConfig HTTPConfig +} + +type HTTPConfig struct { + TLSCert string + TLSKey string + AuthPrivateKey string + AuthPublicKey string + GenerateMissingTLSCert bool + GenerateMissingAuthCert bool + HTTPPort string + HTTPSPort string + ProxyConfig ProxyConfig + Hostname string +} + +type ProxyConfig struct { + Routes []ProxyRouteConfig +} + +type ProxyRouteConfig struct { + Routing Route `validate:"required"` + Target string `validate:"required"` +} + +type Route struct { + UseHost bool + Host string + UsePathPrefix bool + PathPrefix string + Timeout time.Duration + ThrottlePerMinute int + SPAMode bool + CORSOrigin string } \ No newline at end of file diff --git a/src/utils/utils.go b/src/utils/utils.go index eaddc3c..a3b9ac6 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -1,31 +1,30 @@ package utils import ( - "strings" - "log" "os" "net/http" + "encoding/json" + "strconv" + "strings" + "math/rand" ) -func GetRealPath(fullPath string) string { - var dataPathsObject = map[string]string{ - "data": "/mnt/d/", - "diskE": "/mnt/e/", - } +var BaseMainConfig Config +var MainConfig Config +var IsHTTPS = false - path := dataPathsObject[strings.Split(fullPath, "/")[0]] - if path == "" { - log.Println("No path specified") - } - - return path + strings.Join(strings.Split(fullPath, "/")[1:], "/") -} - -func SetHeaders(w http.ResponseWriter) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") +var DefaultConfig = Config{ + LoggingLevel: "INFO", + HTTPConfig: HTTPConfig{ + GenerateMissingTLSCert: true, + GenerateMissingAuthCert: true, + HTTPPort: "80", + HTTPSPort: "443", + Hostname: "0.0.0.0", + ProxyConfig: ProxyConfig{ + Routes: []ProxyRouteConfig{}, + }, + }, } func FileExists(path string) bool { @@ -33,22 +32,163 @@ func FileExists(path string) bool { if err == nil { return true } - log.Println(err) + Error("Reading file error: ", err) return false } -func GetHash() string { - return "hash" -} - func GetRootAppId() string { - return "GUCO" + return "COSMOS" } func GetPrivateAuthKey() string { - return "private" + return MainConfig.HTTPConfig.AuthPrivateKey } -func GenerateRandomString(len int) string { - return "random" +func GetPublicAuthKey() string { + return MainConfig.HTTPConfig.AuthPublicKey +} + +var AlphaNumRunes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +func GenerateRandomString(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = AlphaNumRunes[rand.Intn(len(AlphaNumRunes))] + } + return string(b) +} + +type HTTPErrorResult struct { + Status string `json:"status"` + Message string `json:"message"` + Code string `json:"code"` +} + +func HTTPError(w http.ResponseWriter, message string, code int, userCode string) { + w.WriteHeader(code) + json.NewEncoder(w).Encode(HTTPErrorResult{ + Status: "error", + Message: message, + Code: userCode, + }) + Error("HTTP Request returned Error " + strconv.Itoa(code) + " : " + message, nil) +} + +func SetBaseMainConfig(config Config){ + BaseMainConfig = config + SaveConfigTofile(config) +} + +func LoadBaseMainConfig(config Config){ + BaseMainConfig = config + MainConfig = config + + // use ENV to overwrite configs + + if os.Getenv("HTTP_PORT") != "" { + MainConfig.HTTPConfig.HTTPPort = os.Getenv("HTTP_PORT") + } + if os.Getenv("HTTPS_PORT") != "" { + MainConfig.HTTPConfig.HTTPSPort = os.Getenv("HTTPS_PORT") + } + if os.Getenv("HOSTNAME") != "" { + MainConfig.HTTPConfig.Hostname = os.Getenv("HOSTNAME") + } + if os.Getenv("GENERATE_MISSING_TLS_CERT") != "" { + MainConfig.HTTPConfig.GenerateMissingTLSCert = os.Getenv("GENERATE_MISSING_TLS_CERT") == "true" + } + if os.Getenv("GENERATE_MISSING_AUTH_CERT") != "" { + MainConfig.HTTPConfig.GenerateMissingAuthCert = os.Getenv("GENERATE_MISSING_AUTH_CERT") == "true" + } + if os.Getenv("TLS_CERT") != "" { + MainConfig.HTTPConfig.TLSCert = os.Getenv("TLS_CERT") + } + if os.Getenv("TLS_KEY") != "" { + MainConfig.HTTPConfig.TLSKey = os.Getenv("TLS_KEY") + } + if os.Getenv("AUTH_PRIV_KEY") != "" { + MainConfig.HTTPConfig.AuthPrivateKey = os.Getenv("AUTH_PRIVATE_KEY") + } + if os.Getenv("AUTH_PUBLIC_KEY") != "" { + MainConfig.HTTPConfig.AuthPublicKey = os.Getenv("AUTH_PUBLIC_KEY") + } + if os.Getenv("LOG_LEVEL") != "" { + MainConfig.LoggingLevel = (LoggingLevel)(os.Getenv("LOG_LEVEL")) + } +} + +func GetMainConfig() Config { + return MainConfig +} + +func GetBaseMainConfig() Config { + return BaseMainConfig +} + +func Sanitize(s string) string { + return strings.ToLower(strings.TrimSpace(s)) +} + +func GetConfigFileName() string { + configFile := os.Getenv("CONFIG_FILE") + + if configFile == "" { + configFile = "/config/cosmos.config.json" + } + + + return configFile +} + +func CreateDefaultConfigFileIfNecessary() bool { + configFile := GetConfigFileName() + + // get folder path + folderPath := strings.Split(configFile, "/") + folderPath = folderPath[:len(folderPath)-1] + folderPathString := strings.Join(folderPath, "/") + os.MkdirAll(folderPathString, os.ModePerm) + + if _, err := os.Stat(configFile); os.IsNotExist(err) { + Log("Config file does not exist. Creating default config file.") + file, err := os.Create(configFile) + if err != nil { + Fatal("Creating Default Config File", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(DefaultConfig) + if err != nil { + Fatal("Writing Default Config File", err) + } + + return true + } + return false +} + +func SaveConfigTofile(config Config) { + configFile := GetConfigFileName() + CreateDefaultConfigFileIfNecessary() + + file, err := os.OpenFile(configFile, os.O_WRONLY, os.ModePerm) + if err != nil { + Fatal("Opening Config File", err) + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + err = encoder.Encode(config) + if err != nil { + Fatal("Writing Config File", err) + } + + Log("Config file saved."); +} + +func RestartServer() { + Log("Restarting server...") + os.Exit(0) } \ No newline at end of file diff --git a/test-server.js b/test-server.js index f19835d..09c3324 100644 --- a/test-server.js +++ b/test-server.js @@ -2,7 +2,6 @@ const express = require('express') const app = express() const port = 3000 -var expressWs = require('express-ws')(app); // console log every request sent app.use((req, res, next) => { @@ -30,9 +29,9 @@ app.listen(port, () => { console.log(`Example app listening on port ${port}`) }) -app.ws('/ws', function(ws, req) { - ws.on('message', function(msg) { - console.log(msg); - ws.send(msg); - }); -}); \ No newline at end of file +// app.ws('/ws', function(ws, req) { +// ws.on('message', function(msg) { +// console.log(msg); +// ws.send(msg); +// }); +// }); \ No newline at end of file diff --git a/test-websocket.js b/test-websocket.js deleted file mode 100644 index 3b59b8b..0000000 --- a/test-websocket.js +++ /dev/null @@ -1,16 +0,0 @@ -const WebSocket = require('ws'); - -// send - -const ws = new WebSocket('ws://localhost:8080/proxy/ws', { - rejectUnauthorized: false, - followRedirects : true, - }); - -ws.on('open', function open() { - ws.send('something'); -}); - -ws.on('message', function incoming(data) { - console.log(data); -}); diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 744bb58..0000000 --- a/vite.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - root: 'client', - outDir: 'static', -})