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
+
+# 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',
-})