原始提交
This commit is contained in:
		
							
								
								
									
										25
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					### JetBrains template
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
 | 
					*.iml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Go template
 | 
				
			||||||
 | 
					# Binaries for programs and plugins
 | 
				
			||||||
 | 
					*.exe
 | 
				
			||||||
 | 
					*.exe~
 | 
				
			||||||
 | 
					*.dll
 | 
				
			||||||
 | 
					*.so
 | 
				
			||||||
 | 
					*.dylib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Test binary, built with `go test -c`
 | 
				
			||||||
 | 
					*.test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Output of the go coverage tool, specifically when used with LiteIDE
 | 
				
			||||||
 | 
					*.out
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Dependency directories (remove the comment below to include it)
 | 
				
			||||||
 | 
					# vendor/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					yggdrasil
 | 
				
			||||||
 | 
					/*.pem
 | 
				
			||||||
 | 
					/*.db
 | 
				
			||||||
 | 
					/config.ini
 | 
				
			||||||
							
								
								
									
										661
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										661
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,661 @@
 | 
				
			|||||||
 | 
					                    GNU AFFERO GENERAL PUBLIC LICENSE
 | 
				
			||||||
 | 
					                       Version 3, 19 November 2007
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 | 
				
			||||||
 | 
					 Everyone is permitted to copy and distribute verbatim copies
 | 
				
			||||||
 | 
					 of this license document, but changing it is not allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            Preamble
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  The GNU 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <one line to give the program's name and a brief idea of what it does.>
 | 
				
			||||||
 | 
					    Copyright (C) 2022  sunxi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					<https://www.gnu.org/licenses/>.
 | 
				
			||||||
							
								
								
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					GO_CMD ?= go
 | 
				
			||||||
 | 
					GO_BUILD = $(GO_CMD) build
 | 
				
			||||||
 | 
					GO_CLEAN = $(GO_CMD) clean
 | 
				
			||||||
 | 
					GO_TEST = $(GO_CMD) test
 | 
				
			||||||
 | 
					GO_GET = $(GO_CMD) get
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BINARY = yggdrasil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					PACKAGE_NAME = yggdrasil.tar.gz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					default: $(BINARY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(BINARY):
 | 
				
			||||||
 | 
						$(GO_BUILD) -tags=nomsgpack -o $(BINARY)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package:$(BINARY)
 | 
				
			||||||
 | 
						tar -zcf $(PACKAGE_NAME) $(BINARY) config_example.ini
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					clean:
 | 
				
			||||||
 | 
						-$(GO_CLEAN)
 | 
				
			||||||
 | 
						-rm -rf $(BINARY) $(PACKAGE_NAME)
 | 
				
			||||||
							
								
								
									
										28
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					# Go Yggdrasil Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					使用 Go 语言 Gin + GORM 框架编写的 Minecraft 登录协议服务端。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					+ 实现了 Minecraft 登录服务器时的认证部分以及材质部分。支持注册。
 | 
				
			||||||
 | 
					+ 兼容 [authlib-injector](https://github.com/yushijinhun/authlib-injector) 。
 | 
				
			||||||
 | 
					+ 支持使用在线账号(正版账号)登录,起到透明代理的功能。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 用途
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					用于服务器管理员调试和测试时使用小号登录而不必关闭在线验证 (online-mode)。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					禁止使玩家绕过在线验证登录服务器而不必购买 Minecraft。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					禁止其他违反 [EULA](https://account.mojang.com/documents/minecraft_eula) 的行为。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 用法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					下载或编译得到可执行文件并运行,将会自动生成所需的配置文件和数据库文件。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					配置文件格式详见 `config_example.ini`,请重命名为 `config.ini` 并放在执行目录下。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					启动成功后在启动器(请使用第三方启动器)外置登录选项上填写运行的 URL 的根路径,比如 `http://localhost:8080`。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					注册地址在 `/profile/index.html`。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										86
									
								
								assets/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								assets/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					  ~ Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					  ~
 | 
				
			||||||
 | 
					  ~ 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					  -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="utf-8">
 | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					  <title>注册</title>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="https://cdn.bootcss.com/normalize/8.0.0/normalize.min.css">
 | 
				
			||||||
 | 
					  <!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"> -->
 | 
				
			||||||
 | 
					  <link href="https://cdn.bootcss.com/material-components-web/5.1.0/material-components-web.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					  <script src="https://cdn.bootcss.com/material-components-web/5.1.0/material-components-web.min.js"></script>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="login.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body >
 | 
				
			||||||
 | 
					  <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
				
			||||||
 | 
					  <section class="header">
 | 
				
			||||||
 | 
					    <h1>简陋注册页</h1>
 | 
				
			||||||
 | 
					  </section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <form id="reg-form" action="#">
 | 
				
			||||||
 | 
					    <div class="mdc-text-field username">
 | 
				
			||||||
 | 
					      <input type="email" class="mdc-text-field__input" id="username-input" name="username" required>
 | 
				
			||||||
 | 
					      <label class="mdc-floating-label" for="username-input">邮箱</label>
 | 
				
			||||||
 | 
					      <div class="mdc-line-ripple"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
						<div class="mdc-text-field profileName">
 | 
				
			||||||
 | 
						  <input type="text" class="mdc-text-field__input" id="profileName-input" name="profileName" required minlength="2">
 | 
				
			||||||
 | 
						  <label class="mdc-floating-label" for="profileName-input">角色名</label>
 | 
				
			||||||
 | 
						  <div class="mdc-line-ripple"></div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    <div class="mdc-text-field-helper-line profileName-helper">
 | 
				
			||||||
 | 
					      <div class="mdc-text-field-helper-text" aria-hidden="true">字母,数字或下划线</div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="mdc-text-field password">
 | 
				
			||||||
 | 
					      <input type="password" class="mdc-text-field__input" id="password-input" name="password" required minlength="6">
 | 
				
			||||||
 | 
					      <label class="mdc-floating-label" for="password-input">密码</label>
 | 
				
			||||||
 | 
					      <div class="mdc-line-ripple"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
						<div class="mdc-text-field-helper-line password-helper">
 | 
				
			||||||
 | 
						  <div class="mdc-text-field-helper-text" aria-hidden="true">警告: 暂无重置密码接口,请妥善保管密码</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					    <div class="button-container">
 | 
				
			||||||
 | 
					      <button class="mdc-button mdc-button--raised login">
 | 
				
			||||||
 | 
					        <div class="mdc-button__ripple"></div>
 | 
				
			||||||
 | 
					        <span class="mdc-button__label">
 | 
				
			||||||
 | 
					          已有账号登录
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      <button type="submit" class="mdc-button mdc-button--raised next">
 | 
				
			||||||
 | 
					        <div class="mdc-button__ripple"></div>
 | 
				
			||||||
 | 
					        <span class="mdc-button__label">
 | 
				
			||||||
 | 
					          下一步
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
 | 
					  <div class="mdc-snackbar">
 | 
				
			||||||
 | 
					    <div class="mdc-snackbar__surface">
 | 
				
			||||||
 | 
					      <div class="mdc-snackbar__label"
 | 
				
			||||||
 | 
					           role="status"
 | 
				
			||||||
 | 
					           aria-live="polite">
 | 
				
			||||||
 | 
					        注册失败
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
 | 
				
			||||||
 | 
					  <script src="login.js" async></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										51
									
								
								assets/login.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								assets/login.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html, body {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
					  font-family: 'Roboto';
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding-top: 0.1px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.username,
 | 
				
			||||||
 | 
					.profileName,
 | 
				
			||||||
 | 
					.password,
 | 
				
			||||||
 | 
					.profileName-helper,
 | 
				
			||||||
 | 
					.password-helper {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  margin: 20px auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-container button {
 | 
				
			||||||
 | 
					  margin: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										129
									
								
								assets/login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								assets/login.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MDCSnackbar = mdc.snackbar.MDCSnackbar;
 | 
				
			||||||
 | 
					const MDCTextField = mdc.textField.MDCTextField;
 | 
				
			||||||
 | 
					const MDCRipple = mdc.ripple.MDCRipple;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const snackbar = new MDCSnackbar(document.querySelector(".mdc-snackbar"));
 | 
				
			||||||
 | 
					const username = new MDCTextField(document.querySelector(".username"));
 | 
				
			||||||
 | 
					const password = new MDCTextField(document.querySelector(".password"));
 | 
				
			||||||
 | 
					const profileName = new MDCTextField(document.querySelector(".profileName"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					new MDCRipple(document.querySelector(".next"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					snackbar.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var login = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(".login").click(function (btn) {
 | 
				
			||||||
 | 
					    login = true;
 | 
				
			||||||
 | 
					    $(".profileName").hide();
 | 
				
			||||||
 | 
					    $("#profileName-input").removeAttr("required");
 | 
				
			||||||
 | 
					    $(".next").children(".mdc-button__label").text("登录");
 | 
				
			||||||
 | 
					    $(this).hide();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$("#reg-form").submit(function (e) {
 | 
				
			||||||
 | 
					    if (!login) {
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            url: "/authserver/register",
 | 
				
			||||||
 | 
					            type: "POST",
 | 
				
			||||||
 | 
					            dataType: "JSON",
 | 
				
			||||||
 | 
					            contentType: "application/json",
 | 
				
			||||||
 | 
					            data: JSON.stringify({
 | 
				
			||||||
 | 
					                username: username.value,
 | 
				
			||||||
 | 
					                password: password.value,
 | 
				
			||||||
 | 
					                profileName: profileName.value
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            success: function (data) {
 | 
				
			||||||
 | 
					                if (!data.id) {
 | 
				
			||||||
 | 
					                    if (data.errorMessage) snackbar.labelText = data.errorMessage;
 | 
				
			||||||
 | 
					                    snackbar.open();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    login = true;
 | 
				
			||||||
 | 
					                    $(".profileName").hide();
 | 
				
			||||||
 | 
					                    $(".login").hide();
 | 
				
			||||||
 | 
					                    $(".next").children(".mdc-button__label").text("登录");
 | 
				
			||||||
 | 
					                    snackbar.timeoutMs = 10000;
 | 
				
			||||||
 | 
					                    snackbar.labelText = "注册成功,uid:" + data.id;
 | 
				
			||||||
 | 
					                    snackbar.open();
 | 
				
			||||||
 | 
					                    localStorage.uuid = data.id;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function (e) {
 | 
				
			||||||
 | 
					                let response = JSON.parse(e.responseText);
 | 
				
			||||||
 | 
					                if (response.errorMessage === "profileName exist") {
 | 
				
			||||||
 | 
					                    snackbar.labelText = "注册失败: 角色名已存在";
 | 
				
			||||||
 | 
					                } else if (response.errorMessage === "profileName duplicate") {
 | 
				
			||||||
 | 
					                    snackbar.labelText = "注册失败: 角色名与正版用户冲突";
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    snackbar.labelText = "注册失败: " + response.errorMessage;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            url: "/authserver/authenticate",
 | 
				
			||||||
 | 
					            type: "POST",
 | 
				
			||||||
 | 
					            dataType: "JSON",
 | 
				
			||||||
 | 
					            contentType: "application/json",
 | 
				
			||||||
 | 
					            data: JSON.stringify({
 | 
				
			||||||
 | 
					                username: username.value,
 | 
				
			||||||
 | 
					                password: password.value
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            success: function (data) {
 | 
				
			||||||
 | 
					                if (!data.accessToken) {
 | 
				
			||||||
 | 
					                    snackbar.labelText = "登录失败:";
 | 
				
			||||||
 | 
					                    if (data.errorMessage) snackbar.labelText += data.errorMessage;
 | 
				
			||||||
 | 
					                    snackbar.open();
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                    snackbar.labelText = "登录成功,accessToken:" + data.accessToken;
 | 
				
			||||||
 | 
					                    snackbar.open();
 | 
				
			||||||
 | 
					                    localStorage.accessToken = data.accessToken;
 | 
				
			||||||
 | 
					                    localStorage.loginTime = new Date().getTime();
 | 
				
			||||||
 | 
					                    localStorage.profileName = data.selectedProfile.name;
 | 
				
			||||||
 | 
					                    if (data.selectedProfile) {
 | 
				
			||||||
 | 
					                        localStorage.profileName = data.selectedProfile.name;
 | 
				
			||||||
 | 
					                        localStorage.uuid = data.selectedProfile.id;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    // localStorage.username = username.value;
 | 
				
			||||||
 | 
					                    // localStorage.password = password.value;
 | 
				
			||||||
 | 
					                    setTimeout(function () {
 | 
				
			||||||
 | 
					                        window.location = "user.html";
 | 
				
			||||||
 | 
					                    }, 3000);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function (e) {
 | 
				
			||||||
 | 
					                let response = JSON.parse(e.responseText);
 | 
				
			||||||
 | 
					                snackbar.labelText = "登录失败: " + response.errorMessage;
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(document).ready(function () {
 | 
				
			||||||
 | 
					    if (!localStorage.accessToken && localStorage.loginTime !== undefined &&
 | 
				
			||||||
 | 
					        (new Date().getTime() - localStorage.loginTime) < 30 * 86400 * 1000) {
 | 
				
			||||||
 | 
					        window.location = "user.html";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										48
									
								
								assets/user.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								assets/user.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.header {
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.model,
 | 
				
			||||||
 | 
					.textureType,
 | 
				
			||||||
 | 
					.url,
 | 
				
			||||||
 | 
					.changeTo {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  margin: 20px auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.file {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    width: 300px;
 | 
				
			||||||
 | 
					    margin: 20px auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-container {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
					  width: 300px;
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-container button {
 | 
				
			||||||
 | 
					  margin: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										154
									
								
								assets/user.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								assets/user.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					<!--
 | 
				
			||||||
 | 
					  ~ Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					  ~
 | 
				
			||||||
 | 
					  ~ 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					  -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<!doctype html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
				
			||||||
 | 
					    <title>角色信息</title>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="https://cdn.bootcss.com/normalize/8.0.0/normalize.min.css">
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
 | 
				
			||||||
 | 
					    <link href="https://cdn.bootcss.com/material-components-web/5.1.0/material-components-web.min.css" rel="stylesheet">
 | 
				
			||||||
 | 
					    <script src="https://cdn.bootcss.com/material-components-web/5.1.0/material-components-web.min.js"></script>
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="user.css">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					<noscript>You need to enable JavaScript to run this app.</noscript>
 | 
				
			||||||
 | 
					<section class="header">
 | 
				
			||||||
 | 
					    <h1>简陋信息页</h1>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section class="header">
 | 
				
			||||||
 | 
					    <h3>上传材质</h3>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<form id="upload-form" action="#" enctype="multipart/form-data">
 | 
				
			||||||
 | 
					    <div class="mdc-form-field textureType">
 | 
				
			||||||
 | 
					        <label>材质类别: </label>
 | 
				
			||||||
 | 
					        <div id="radio-skin" class="mdc-radio">
 | 
				
			||||||
 | 
					            <input class="mdc-radio__native-control" type="radio" id="radio-3" name="type" value="skin" checked>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__background">
 | 
				
			||||||
 | 
					                <div class="mdc-radio__outer-circle"></div>
 | 
				
			||||||
 | 
					                <div class="mdc-radio__inner-circle"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__ripple"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <label for="radio-3">皮肤</label>
 | 
				
			||||||
 | 
					        <div id="radio-cape" class="mdc-radio">
 | 
				
			||||||
 | 
					            <input class="mdc-radio__native-control" type="radio" id="radio-4" name="type" value="cape">
 | 
				
			||||||
 | 
					            <div class="mdc-radio__background">
 | 
				
			||||||
 | 
					                <div class="mdc-radio__outer-circle"></div>
 | 
				
			||||||
 | 
					                <div class="mdc-radio__inner-circle"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__ripple"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <label for="radio-3">披风</label>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="mdc-form-field model">
 | 
				
			||||||
 | 
					        <label>材质模型: </label>
 | 
				
			||||||
 | 
					        <div id="radio-steve" class="mdc-radio">
 | 
				
			||||||
 | 
					            <input class="mdc-radio__native-control" type="radio" id="radio-1" name="model" value="default" checked>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__background">
 | 
				
			||||||
 | 
					                <div class="mdc-radio__outer-circle"></div>
 | 
				
			||||||
 | 
					                <div class="mdc-radio__inner-circle"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__ripple"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <label for="radio-1">Steve</label>
 | 
				
			||||||
 | 
					        <div id="radio-alex" class="mdc-radio">
 | 
				
			||||||
 | 
					            <input class="mdc-radio__native-control" type="radio" id="radio-2" name="model" value="slim">
 | 
				
			||||||
 | 
					            <div class="mdc-radio__background">
 | 
				
			||||||
 | 
					                <div class="mdc-radio__outer-circle"></div>
 | 
				
			||||||
 | 
					                <div class="mdc-radio__inner-circle"></div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mdc-radio__ripple"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <label for="radio-2">Alex</label>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="mdc-text-field url">
 | 
				
			||||||
 | 
					        <input type="url" class="mdc-text-field__input" id="url-input" name="url">
 | 
				
			||||||
 | 
					        <label class="mdc-floating-label" for="url-input">材质url</label>
 | 
				
			||||||
 | 
					        <div class="mdc-line-ripple"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <label class="mdc-text-field mdc-text-field--outlined mdc-text-field--with-trailing-icon file">
 | 
				
			||||||
 | 
					        <input type="file" style="display: none;" accept="image/*" class="mdc-text-field__input" id="file-input"
 | 
				
			||||||
 | 
					               name="file" aria-label="Label">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <i class="material-icons mdc-text-field__icon mdc-text-field__icon--trailing" tabindex="0" role="button"
 | 
				
			||||||
 | 
					           onclick="$('#file-input').val('').change();">delete</i>
 | 
				
			||||||
 | 
					        <div class="mdc-notched-outline">
 | 
				
			||||||
 | 
					            <div class="mdc-notched-outline__leading"></div>
 | 
				
			||||||
 | 
					            <div class="mdc-notched-outline__notch">
 | 
				
			||||||
 | 
					                <label id="file-path" for="file-input" class="mdc-floating-label">或者选择一个图片</label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="mdc-notched-outline__trailing"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					    <div class="button-container">
 | 
				
			||||||
 | 
					        <button type="submit" class="mdc-button mdc-button--raised upload">
 | 
				
			||||||
 | 
					            <div class="mdc-button__ripple"></div>
 | 
				
			||||||
 | 
					            <span class="mdc-button__label">
 | 
				
			||||||
 | 
					          上传
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <button type="button" id="delete-btn" class="mdc-button mdc-button--raised upload">
 | 
				
			||||||
 | 
					            <div class="mdc-button__ripple"></div>
 | 
				
			||||||
 | 
					            <span class="mdc-button__label">
 | 
				
			||||||
 | 
					          重置为默认
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section class="header">
 | 
				
			||||||
 | 
					    <h3>更改游戏标签</h3>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<form id="change-form" action="#">
 | 
				
			||||||
 | 
					    <div class="mdc-text-field changeTo">
 | 
				
			||||||
 | 
					        <input type="text" class="mdc-text-field__input" id="changeTo-input" name="changeTo" required minlength="2"
 | 
				
			||||||
 | 
					               maxlength="16">
 | 
				
			||||||
 | 
					        <script>
 | 
				
			||||||
 | 
					            if (localStorage.profileName) document.getElementById("changeTo-input").value = localStorage.profileName;
 | 
				
			||||||
 | 
					        </script>
 | 
				
			||||||
 | 
					        <label class="mdc-floating-label" for="changeTo-input">游戏标签</label>
 | 
				
			||||||
 | 
					        <div class="mdc-line-ripple"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="button-container">
 | 
				
			||||||
 | 
					        <button type="submit" class="mdc-button mdc-button--raised submit">
 | 
				
			||||||
 | 
					            <div class="mdc-button__ripple"></div>
 | 
				
			||||||
 | 
					            <span class="mdc-button__label">
 | 
				
			||||||
 | 
					            更改
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</form>
 | 
				
			||||||
 | 
					<div class="mdc-snackbar">
 | 
				
			||||||
 | 
					    <div class="mdc-snackbar__surface">
 | 
				
			||||||
 | 
					        <div class="mdc-snackbar__label"
 | 
				
			||||||
 | 
					             role="status"
 | 
				
			||||||
 | 
					             aria-live="polite">
 | 
				
			||||||
 | 
					            上传失败
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
 | 
				
			||||||
 | 
					<script src="user.js" async></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										242
									
								
								assets/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										242
									
								
								assets/user.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,242 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MDCSnackbar = mdc.snackbar.MDCSnackbar;
 | 
				
			||||||
 | 
					const MDCTextField = mdc.textField.MDCTextField;
 | 
				
			||||||
 | 
					const MDCFormField = mdc.formField.MDCFormField;
 | 
				
			||||||
 | 
					const MDCRadio = mdc.radio.MDCRadio;
 | 
				
			||||||
 | 
					//const MDCRipple = mdc.ripple.MDCRipple;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const snackbar = new MDCSnackbar(document.querySelector('.mdc-snackbar'));
 | 
				
			||||||
 | 
					const modelField = new MDCFormField(document.querySelector('.model'));
 | 
				
			||||||
 | 
					const radio1 = new MDCRadio(document.querySelector('#radio-steve'));
 | 
				
			||||||
 | 
					const radio2 = new MDCRadio(document.querySelector('#radio-alex'));
 | 
				
			||||||
 | 
					const radio3 = new MDCRadio(document.querySelector('#radio-skin'));
 | 
				
			||||||
 | 
					const radio4 = new MDCRadio(document.querySelector('#radio-cape'));
 | 
				
			||||||
 | 
					modelField.input = {radio1, radio2};
 | 
				
			||||||
 | 
					const url = new MDCTextField(document.querySelector('.url'));
 | 
				
			||||||
 | 
					const file = new MDCTextField(document.querySelector('.file'));
 | 
				
			||||||
 | 
					const changeTo = new MDCTextField(document.querySelector('.changeTo'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const modelTypeForm = document.querySelector('.mdc-form-field.model');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					snackbar.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$("#file-input").change(function() {
 | 
				
			||||||
 | 
					    const path = this.value;
 | 
				
			||||||
 | 
					    if (!path) {
 | 
				
			||||||
 | 
					        $("#file-path").text("或选择一张图片");
 | 
				
			||||||
 | 
					        $(".url").show();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        $("#file-path").text(path);
 | 
				
			||||||
 | 
					        url.value = '';
 | 
				
			||||||
 | 
					        $(".url").hide();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const modelTypeChange = function () {
 | 
				
			||||||
 | 
					    if (radio3.checked) {
 | 
				
			||||||
 | 
					        $(modelTypeForm).show();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        $(modelTypeForm).hide();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					$("#radio-3").change(modelTypeChange)
 | 
				
			||||||
 | 
					$("#radio-4").change(modelTypeChange)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$("#url-input").on("input", function() {
 | 
				
			||||||
 | 
					    const url = this.value;
 | 
				
			||||||
 | 
					    if (!url) {
 | 
				
			||||||
 | 
					        $(".file").show();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        file.value = '';
 | 
				
			||||||
 | 
					        $(".file").hide();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(document).ready(function() {
 | 
				
			||||||
 | 
					    if (!localStorage.accessToken) {
 | 
				
			||||||
 | 
					        localStorage.loginTime = 1;
 | 
				
			||||||
 | 
					        window.location = "index.html";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    $.ajax({
 | 
				
			||||||
 | 
					        url: '/authserver/validate',
 | 
				
			||||||
 | 
					        type: 'POST',
 | 
				
			||||||
 | 
					        dataType: "JSON",
 | 
				
			||||||
 | 
					        contentType: "application/json",
 | 
				
			||||||
 | 
					        data: JSON.stringify({
 | 
				
			||||||
 | 
					            accessToken: localStorage.accessToken,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        success: function(data) {
 | 
				
			||||||
 | 
					            //有效,啥也不整
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        error: function(e) {
 | 
				
			||||||
 | 
					            if (e.status == 403) {
 | 
				
			||||||
 | 
					                // 持续套娃
 | 
				
			||||||
 | 
					                $.ajax({
 | 
				
			||||||
 | 
					                    url: '/authserver/refresh',
 | 
				
			||||||
 | 
					                    type: 'POST',
 | 
				
			||||||
 | 
					                    dataType: "JSON",
 | 
				
			||||||
 | 
					                    contentType: "application/json",
 | 
				
			||||||
 | 
					                    data: JSON.stringify({
 | 
				
			||||||
 | 
					                        accessToken: localStorage.accessToken,
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                    success: function(data) {
 | 
				
			||||||
 | 
					                        if (!data.accessToken) {
 | 
				
			||||||
 | 
					                            localStorage.loginTime = 1;
 | 
				
			||||||
 | 
					                            window.location = "index.html";
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                            snackbar.labelText = "刷新token成功,accessToken:" + data.accessToken;
 | 
				
			||||||
 | 
					                            snackbar.open();
 | 
				
			||||||
 | 
					                            localStorage.accessToken = data.accessToken;
 | 
				
			||||||
 | 
					                            localStorage.loginTime = new Date().getTime();
 | 
				
			||||||
 | 
					                            if(data.selectedProfile) {
 | 
				
			||||||
 | 
					                                localStorage.profileName = data.selectedProfile.name;
 | 
				
			||||||
 | 
					                                localStorage.uuid = data.selectedProfile.id;
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    error: function(e) {
 | 
				
			||||||
 | 
					                        if (e.status == 403) {
 | 
				
			||||||
 | 
					                            localStorage.loginTime = 1;
 | 
				
			||||||
 | 
					                            window.location = "index.html";
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$("#upload-form").submit(function(e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    if (!url.value && !$("#file-input").val()) {
 | 
				
			||||||
 | 
					        snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					        snackbar.labelText = "没填信息";
 | 
				
			||||||
 | 
					        snackbar.open();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let textureType = 'skin'
 | 
				
			||||||
 | 
					    if (radio3.checked) {
 | 
				
			||||||
 | 
					        textureType = 'skin'
 | 
				
			||||||
 | 
					    } else if (radio4.checked) {
 | 
				
			||||||
 | 
					        textureType = 'cape'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!url.value) {
 | 
				
			||||||
 | 
					        const formData = new FormData();
 | 
				
			||||||
 | 
					        formData.append("model", radio1.checked ? radio1.value : radio2.value);
 | 
				
			||||||
 | 
					        formData.append("file", $("#file-input")[0].files[0]);
 | 
				
			||||||
 | 
					        //formData.contentType = "multipart/form-data";
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            url: `/api/user/profile/${localStorage.uuid}/${textureType}`,
 | 
				
			||||||
 | 
					            type: 'PUT',
 | 
				
			||||||
 | 
					            processData: false,
 | 
				
			||||||
 | 
					            contentType: false,
 | 
				
			||||||
 | 
					            headers: {'Authorization':'Bearer ' + localStorage.accessToken},
 | 
				
			||||||
 | 
					            data: formData,
 | 
				
			||||||
 | 
					            success: function(data) {
 | 
				
			||||||
 | 
					                snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                snackbar.labelText = "材质上传成功";
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function(e) {
 | 
				
			||||||
 | 
					                snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                snackbar.labelText = "材质上传失败";
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else if (url.value) {
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            url: `/api/user/profile/${localStorage.uuid}/${textureType}`,
 | 
				
			||||||
 | 
					            type: 'POST',
 | 
				
			||||||
 | 
					            dataType: "JSON",
 | 
				
			||||||
 | 
					            contentType: "application/json",
 | 
				
			||||||
 | 
					            headers: {'Authorization':'Bearer ' + localStorage.accessToken},
 | 
				
			||||||
 | 
					            data: JSON.stringify({
 | 
				
			||||||
 | 
					                model: radio1.checked ? radio1.value : radio2.value,
 | 
				
			||||||
 | 
					                url: url.value
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            success: function(data) {
 | 
				
			||||||
 | 
					                snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                snackbar.labelText = "材质上传成功";
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function(e) {
 | 
				
			||||||
 | 
					                snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					                snackbar.labelText = "材质上传失败";
 | 
				
			||||||
 | 
					                snackbar.open();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$("#change-form").submit(function(e) {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    if (changeTo.value.length <= 1) {
 | 
				
			||||||
 | 
					        snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					        snackbar.labelText = "更改失败, 角色名格式不正确";
 | 
				
			||||||
 | 
					        snackbar.open();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    $.ajax({
 | 
				
			||||||
 | 
					        url: '/authserver/change',
 | 
				
			||||||
 | 
					        type: 'POST',
 | 
				
			||||||
 | 
					        dataType: "JSON",
 | 
				
			||||||
 | 
					        contentType: "application/json",
 | 
				
			||||||
 | 
					        data: JSON.stringify({
 | 
				
			||||||
 | 
					            accessToken: localStorage.accessToken,
 | 
				
			||||||
 | 
					            changeTo: changeTo.value
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        success: function(data) {
 | 
				
			||||||
 | 
					            snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					            snackbar.labelText = "更改成功";
 | 
				
			||||||
 | 
					            snackbar.open();
 | 
				
			||||||
 | 
					            localStorage.profileName = changeTo.value;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        error: function(e) {
 | 
				
			||||||
 | 
					            snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					            snackbar.labelText = "更改失败, 可能是角色名已存在";
 | 
				
			||||||
 | 
					            snackbar.open();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$('#delete-btn').click(function () {
 | 
				
			||||||
 | 
					    let textureType = 'skin'
 | 
				
			||||||
 | 
					    if (radio3.checked) {
 | 
				
			||||||
 | 
					        textureType = 'skin'
 | 
				
			||||||
 | 
					    } else if (radio4.checked) {
 | 
				
			||||||
 | 
					        textureType = 'cape'
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    $.ajax({
 | 
				
			||||||
 | 
					        url: `/api/user/profile/${localStorage.uuid}/${textureType}`,
 | 
				
			||||||
 | 
					        type: 'DELETE',
 | 
				
			||||||
 | 
					        headers: {'Authorization':'Bearer ' + localStorage.accessToken},
 | 
				
			||||||
 | 
					        success: function(data) {
 | 
				
			||||||
 | 
					            snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					            snackbar.labelText = "恢复成功";
 | 
				
			||||||
 | 
					            snackbar.open();
 | 
				
			||||||
 | 
					            localStorage.profileName = changeTo.value;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        error: function(e) {
 | 
				
			||||||
 | 
					            snackbar.timeoutMs = 5000;
 | 
				
			||||||
 | 
					            snackbar.labelText = "重置失败";
 | 
				
			||||||
 | 
					            snackbar.open();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										29
									
								
								config_example.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								config_example.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					[meta]
 | 
				
			||||||
 | 
					;应用显示名称
 | 
				
			||||||
 | 
					server_name            = A Mojang Yggdrasil Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;软件名称
 | 
				
			||||||
 | 
					implementation_name    = go-yggdrasil-server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;版本
 | 
				
			||||||
 | 
					implementation_version = v0.1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;皮肤、披风材质白名单
 | 
				
			||||||
 | 
					skin_domains           = .example.com, localhost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;访问路径(不要添加"/"后缀)
 | 
				
			||||||
 | 
					skin_root_url          = http://localhost:8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[server]
 | 
				
			||||||
 | 
					;服务监听地址
 | 
				
			||||||
 | 
					server_address = :8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[paths]
 | 
				
			||||||
 | 
					;数据库存储路径
 | 
				
			||||||
 | 
					database_file    = sqlite.db
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;私钥存储路径
 | 
				
			||||||
 | 
					private_key_file = private.pem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;公钥存储路径
 | 
				
			||||||
 | 
					public_key_file  = public.pem
 | 
				
			||||||
							
								
								
									
										35
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					module yggdrasil-go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.17
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/gin-gonic/gin v1.7.7
 | 
				
			||||||
 | 
						github.com/google/uuid v1.3.0
 | 
				
			||||||
 | 
						github.com/hashicorp/golang-lru v0.5.4
 | 
				
			||||||
 | 
						golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
 | 
				
			||||||
 | 
						golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
 | 
				
			||||||
 | 
						gopkg.in/ini.v1 v1.66.4
 | 
				
			||||||
 | 
						gorm.io/driver/sqlite v1.2.6
 | 
				
			||||||
 | 
						gorm.io/gorm v1.22.3
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/gin-contrib/sse v0.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/go-playground/locales v0.13.0 // indirect
 | 
				
			||||||
 | 
						github.com/go-playground/universal-translator v0.17.0 // indirect
 | 
				
			||||||
 | 
						github.com/go-playground/validator/v10 v10.4.1 // indirect
 | 
				
			||||||
 | 
						github.com/golang/protobuf v1.3.3 // indirect
 | 
				
			||||||
 | 
						github.com/jinzhu/inflection v1.0.0 // indirect
 | 
				
			||||||
 | 
						github.com/jinzhu/now v1.1.4 // indirect
 | 
				
			||||||
 | 
						github.com/json-iterator/go v1.1.9 // indirect
 | 
				
			||||||
 | 
						github.com/kr/pretty v0.3.0 // indirect
 | 
				
			||||||
 | 
						github.com/leodido/go-urn v1.2.0 // indirect
 | 
				
			||||||
 | 
						github.com/mattn/go-isatty v0.0.12 // indirect
 | 
				
			||||||
 | 
						github.com/mattn/go-sqlite3 v1.14.9 // indirect
 | 
				
			||||||
 | 
						github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
 | 
				
			||||||
 | 
						github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
 | 
				
			||||||
 | 
						github.com/ugorji/go/codec v1.1.7 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
 | 
				
			||||||
 | 
						gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 | 
				
			||||||
 | 
						gopkg.in/yaml.v2 v2.2.8 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										90
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 | 
				
			||||||
 | 
					github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 | 
				
			||||||
 | 
					github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
 | 
				
			||||||
 | 
					github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
 | 
				
			||||||
 | 
					github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
 | 
				
			||||||
 | 
					github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 | 
				
			||||||
 | 
					github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
 | 
				
			||||||
 | 
					github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
 | 
				
			||||||
 | 
					github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
 | 
				
			||||||
 | 
					github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 | 
				
			||||||
 | 
					github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 | 
				
			||||||
 | 
					github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
 | 
				
			||||||
 | 
					github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 | 
				
			||||||
 | 
					github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
				
			||||||
 | 
					github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 | 
				
			||||||
 | 
					github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 | 
				
			||||||
 | 
					github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 | 
				
			||||||
 | 
					github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 | 
				
			||||||
 | 
					github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
				
			||||||
 | 
					github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
 | 
				
			||||||
 | 
					github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 | 
				
			||||||
 | 
					github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
 | 
				
			||||||
 | 
					github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 | 
				
			||||||
 | 
					github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 | 
				
			||||||
 | 
					github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 | 
				
			||||||
 | 
					github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 | 
				
			||||||
 | 
					github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
				
			||||||
 | 
					github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
				
			||||||
 | 
					github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
				
			||||||
 | 
					github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
				
			||||||
 | 
					github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 | 
				
			||||||
 | 
					github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 | 
				
			||||||
 | 
					github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 | 
				
			||||||
 | 
					github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 | 
				
			||||||
 | 
					github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
 | 
				
			||||||
 | 
					github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
				
			||||||
 | 
					github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
 | 
				
			||||||
 | 
					github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
 | 
				
			||||||
 | 
					github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
 | 
					github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
 | 
				
			||||||
 | 
					github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 | 
				
			||||||
 | 
					github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 | 
				
			||||||
 | 
					github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
				
			||||||
 | 
					golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
 | 
					golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
				
			||||||
 | 
					golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
 | 
				
			||||||
 | 
					golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
				
			||||||
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 | 
				
			||||||
 | 
					gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
 | 
				
			||||||
 | 
					gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
 | 
					gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
 | 
				
			||||||
 | 
					gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
 | 
				
			||||||
 | 
					gorm.io/gorm v1.22.3 h1:/JS6z+GStEQvJNW3t1FTwJwG/gZ+A7crFdRqtvG5ehA=
 | 
				
			||||||
 | 
					gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
 | 
				
			||||||
							
								
								
									
										192
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,192 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/x509"
 | 
				
			||||||
 | 
						"embed"
 | 
				
			||||||
 | 
						"encoding/pem"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"gopkg.in/ini.v1"
 | 
				
			||||||
 | 
						"gorm.io/driver/sqlite"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/router"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//go:embed assets/*
 | 
				
			||||||
 | 
					var f embed.FS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MetaCfg struct {
 | 
				
			||||||
 | 
						ServerName            string   `ini:"server_name"`
 | 
				
			||||||
 | 
						ImplementationName    string   `ini:"implementation_name"`
 | 
				
			||||||
 | 
						ImplementationVersion string   `ini:"implementation_version"`
 | 
				
			||||||
 | 
						SkinDomains           []string `ini:"skin_domains"`
 | 
				
			||||||
 | 
						SkinRootUrl           string   `ini:"skin_root_url"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						configFilePath := "config.ini"
 | 
				
			||||||
 | 
						cfg, err := ini.LooseLoad(configFilePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal("无法读取配置文件", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						meta := MetaCfg{
 | 
				
			||||||
 | 
							ServerName:            "A Mojang Yggdrasil Server",
 | 
				
			||||||
 | 
							ImplementationName:    "go-yggdrasil-server",
 | 
				
			||||||
 | 
							ImplementationVersion: "v0.1",
 | 
				
			||||||
 | 
							SkinDomains:           []string{".example.com", "localhost"},
 | 
				
			||||||
 | 
							SkinRootUrl:           "http://localhost:8080",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = cfg.Section("meta").MapTo(&meta)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal("无法读取配置文件", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pathSection := cfg.Section("paths")
 | 
				
			||||||
 | 
						privateKeyPath := pathSection.Key("private_key_file").MustString("private.pem")
 | 
				
			||||||
 | 
						publicKeyPath := pathSection.Key("public_key_file").MustString("public.pem")
 | 
				
			||||||
 | 
						databasePath := pathSection.Key("database_file").MustString("sqlite.db")
 | 
				
			||||||
 | 
						address := cfg.Section("server").Key("server_address").MustString(":8080")
 | 
				
			||||||
 | 
						_, err = os.Stat(configFilePath)
 | 
				
			||||||
 | 
						if err != nil && os.IsNotExist(err) {
 | 
				
			||||||
 | 
							log.Println("配置文件不存在,已使用默认配置")
 | 
				
			||||||
 | 
							_ = cfg.Section("meta").ReflectFrom(&meta)
 | 
				
			||||||
 | 
							err = cfg.SaveToIndent(configFilePath, " ")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Println("警告: 无法保存配置文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						checkRsaKeyFile(privateKeyPath, publicKeyPath)
 | 
				
			||||||
 | 
						publicKeyContent, err := ioutil.ReadFile(publicKeyPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal("无法读取公钥内容", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						db, err := gorm.Open(sqlite.Open("file:"+databasePath+"?cache=shared"), &gorm.Config{
 | 
				
			||||||
 | 
							SkipDefaultTransaction: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal("无法连接数据库", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = db.AutoMigrate(&model.User{}, &model.Texture{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal("无法导入数据库", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						serverMeta := router.ServerMeta{}
 | 
				
			||||||
 | 
						serverMeta.Meta.ServerName = meta.ServerName
 | 
				
			||||||
 | 
						serverMeta.Meta.ImplementationName = meta.ImplementationName
 | 
				
			||||||
 | 
						serverMeta.Meta.ImplementationVersion = meta.ImplementationVersion
 | 
				
			||||||
 | 
						serverMeta.Meta.FeatureNoMojangNamespace = true
 | 
				
			||||||
 | 
						serverMeta.Meta.Links.Homepage = meta.SkinRootUrl + "/profile/user.html"
 | 
				
			||||||
 | 
						serverMeta.Meta.Links.Register = meta.SkinRootUrl + "/profile/index.html"
 | 
				
			||||||
 | 
						serverMeta.SkinDomains = meta.SkinDomains
 | 
				
			||||||
 | 
						serverMeta.SignaturePublickey = string(publicKeyContent)
 | 
				
			||||||
 | 
						gin.SetMode(gin.ReleaseMode)
 | 
				
			||||||
 | 
						r := gin.Default()
 | 
				
			||||||
 | 
						router.InitRouters(r, db, &serverMeta, meta.SkinRootUrl)
 | 
				
			||||||
 | 
						assetsFs, err := fs.Sub(f, "assets")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						r.StaticFS("/profile", http.FS(assetsFs))
 | 
				
			||||||
 | 
						srv := &http.Server{
 | 
				
			||||||
 | 
							Addr:    address,
 | 
				
			||||||
 | 
							Handler: r,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
 | 
				
			||||||
 | 
								log.Printf("listen: %s\n", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						log.Printf("已启动, 地址: %s\n", srv.Addr)
 | 
				
			||||||
 | 
						quit := make(chan os.Signal)
 | 
				
			||||||
 | 
						signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
 | 
				
			||||||
 | 
						<-quit
 | 
				
			||||||
 | 
						log.Println("关闭...")
 | 
				
			||||||
 | 
						ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
				
			||||||
 | 
						defer cancel()
 | 
				
			||||||
 | 
						if err := srv.Shutdown(ctx); err != nil {
 | 
				
			||||||
 | 
							log.Fatal("强制关闭:", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						log.Println("退出")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func checkRsaKeyFile(privateKeyPath string, publicKeyPath string) {
 | 
				
			||||||
 | 
						_, err := os.Stat(privateKeyPath)
 | 
				
			||||||
 | 
						if err != nil && os.IsNotExist(err) {
 | 
				
			||||||
 | 
							privatePem, err := os.OpenFile(privateKeyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法创建私钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer privatePem.Close()
 | 
				
			||||||
 | 
							publicPem, err := os.OpenFile(publicKeyPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法创建公钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer publicPem.Close()
 | 
				
			||||||
 | 
							privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
 | 
				
			||||||
 | 
							util.PrivateKey = privateKey
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法生成 RSA 密钥", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法序列化 RSA 密钥", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err = pem.Encode(privatePem, &pem.Block{
 | 
				
			||||||
 | 
								Type:  "PRIVATE",
 | 
				
			||||||
 | 
								Bytes: privateKeyBytes,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法写入私钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
 | 
				
			||||||
 | 
							err = pem.Encode(publicPem, &pem.Block{
 | 
				
			||||||
 | 
								Type:  "PUBLIC",
 | 
				
			||||||
 | 
								Bytes: publicKeyBytes,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法写入公钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if err != nil {
 | 
				
			||||||
 | 
							log.Fatalln("无法打开私钥文件", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							pemContent, err := os.ReadFile(privateKeyPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法打开私钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pemBlock, _ := pem.Decode(pemContent)
 | 
				
			||||||
 | 
							privateKeyI, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalln("无法解析私钥文件", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							util.PrivateKey = privateKeyI.(*rsa.PrivateKey)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										40
									
								
								model/authentication_session.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								model/authentication_session.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AuthenticationSession struct {
 | 
				
			||||||
 | 
						ServerId string
 | 
				
			||||||
 | 
						Token    Token
 | 
				
			||||||
 | 
						Ip       string
 | 
				
			||||||
 | 
						createAt int64
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewAuthenticationSession(serverId string, token *Token, ip string) (session AuthenticationSession) {
 | 
				
			||||||
 | 
						session.ServerId = serverId
 | 
				
			||||||
 | 
						session.Token = *token
 | 
				
			||||||
 | 
						session.Ip = ip
 | 
				
			||||||
 | 
						session.createAt = time.Now().UnixMilli()
 | 
				
			||||||
 | 
						return session
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *AuthenticationSession) HasExpired() bool {
 | 
				
			||||||
 | 
						d := time.Now().Sub(time.UnixMilli(s.createAt))
 | 
				
			||||||
 | 
						return d > 30*time.Second
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										130
									
								
								model/profile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								model/profile.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Profile struct {
 | 
				
			||||||
 | 
						Id        uuid.UUID
 | 
				
			||||||
 | 
						Name      string
 | 
				
			||||||
 | 
						ModelType ModelType
 | 
				
			||||||
 | 
						Textures  map[string]string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ModelType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						STEVE ModelType = "default"
 | 
				
			||||||
 | 
						ALEX            = "slim"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MetadataType map[string]interface{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SkinTexture struct {
 | 
				
			||||||
 | 
						Url      string        `json:"url"`
 | 
				
			||||||
 | 
						Metadata *MetadataType `json:"metadata,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type CapeTexture struct {
 | 
				
			||||||
 | 
						Url string `json:"url"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TexturesType struct {
 | 
				
			||||||
 | 
						SKIN *SkinTexture `json:"SKIN,omitempty"`
 | 
				
			||||||
 | 
						CAPE *CapeTexture `json:"CAPE,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewProfile(id uuid.UUID, name string, modelType ModelType, serializedTextures string) (this Profile) {
 | 
				
			||||||
 | 
						this.Id = id
 | 
				
			||||||
 | 
						this.Name = name
 | 
				
			||||||
 | 
						this.ModelType = modelType
 | 
				
			||||||
 | 
						if len(serializedTextures) < 2 {
 | 
				
			||||||
 | 
							serializedTextures = "{}"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := json.Unmarshal([]byte(serializedTextures), &this.Textures)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return this
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProfileResponse struct {
 | 
				
			||||||
 | 
						Name string `json:"name" binding:"required"`
 | 
				
			||||||
 | 
						Id   string `json:"id" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Profile) ToSimpleResponse() ProfileResponse {
 | 
				
			||||||
 | 
						return ProfileResponse{
 | 
				
			||||||
 | 
							Id:   util.UnsignedString(p.Id),
 | 
				
			||||||
 | 
							Name: p.Name,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Profile) ToCompleteResponse(signed bool, textureBaseUrl string) (map[string]interface{}, error) {
 | 
				
			||||||
 | 
						textures := TexturesType{}
 | 
				
			||||||
 | 
						if hash, ok := p.Textures["SKIN"]; ok {
 | 
				
			||||||
 | 
							skin := SkinTexture{
 | 
				
			||||||
 | 
								Url: textureBaseUrl + "/" + hash,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if p.ModelType == ALEX {
 | 
				
			||||||
 | 
								m := MetadataType{
 | 
				
			||||||
 | 
									"model": ALEX,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								skin.Metadata = &m
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							textures.SKIN = &skin
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if hash, ok := p.Textures["CAPE"]; ok {
 | 
				
			||||||
 | 
							cape := CapeTexture{
 | 
				
			||||||
 | 
								Url: textureBaseUrl + "/" + hash,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							textures.CAPE = &cape
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						texturesStr, err := util.EncodeBase64(util.Property{
 | 
				
			||||||
 | 
							Name: "timestamp", Value: time.Now().UnixMilli(),
 | 
				
			||||||
 | 
						}, util.Property{
 | 
				
			||||||
 | 
							Name: "profileId", Value: util.UnsignedString(p.Id),
 | 
				
			||||||
 | 
						}, util.Property{
 | 
				
			||||||
 | 
							Name: "profileName", Value: p.Name,
 | 
				
			||||||
 | 
						}, util.Property{
 | 
				
			||||||
 | 
							Name: "textures", Value: textures,
 | 
				
			||||||
 | 
						}, util.Property{
 | 
				
			||||||
 | 
							Name: "signatureRequired", Value: signed,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						properties := util.Properties(signed,
 | 
				
			||||||
 | 
							util.StringProperty{Name: "textures", Value: texturesStr},
 | 
				
			||||||
 | 
							util.StringProperty{Name: "uploadableTextures", Value: "skin,cape"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						return map[string]interface{}{
 | 
				
			||||||
 | 
							"id":         util.UnsignedString(p.Id),
 | 
				
			||||||
 | 
							"name":       p.Name,
 | 
				
			||||||
 | 
							"properties": properties,
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (p *Profile) Equals(another *Profile) bool {
 | 
				
			||||||
 | 
						return p == another || p.Id == another.Id
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								model/texture.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								model/texture.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/sha256"
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"image"
 | 
				
			||||||
 | 
						"image/color"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Texture struct {
 | 
				
			||||||
 | 
						Hash      string `gorm:"size:64;primaryKey"`
 | 
				
			||||||
 | 
						CreatedAt time.Time
 | 
				
			||||||
 | 
						UpdatedAt time.Time
 | 
				
			||||||
 | 
						Data      []byte `gorm:"not null"`
 | 
				
			||||||
 | 
						Used      uint   `gorm:"not null"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ComputeTextureId(img image.Image) string {
 | 
				
			||||||
 | 
						digest := sha256.New()
 | 
				
			||||||
 | 
						bound := img.Bounds()
 | 
				
			||||||
 | 
						width := bound.Dx()
 | 
				
			||||||
 | 
						height := bound.Dy()
 | 
				
			||||||
 | 
						var buf [4096]byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						putInt(buf[:], int32(width))
 | 
				
			||||||
 | 
						putInt(buf[4:], int32(height))
 | 
				
			||||||
 | 
						var pos = 8
 | 
				
			||||||
 | 
						for x := 0; x < width; x++ {
 | 
				
			||||||
 | 
							for y := 0; y < height; y++ {
 | 
				
			||||||
 | 
								rgba := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
 | 
				
			||||||
 | 
								if rgba.A == 0 {
 | 
				
			||||||
 | 
									copy(buf[pos:], []byte{0, 0, 0, 0})
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									copy(buf[pos:], []byte{rgba.A, rgba.R, rgba.G, rgba.B})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								pos += 4
 | 
				
			||||||
 | 
								if pos == len(buf) {
 | 
				
			||||||
 | 
									pos = 0
 | 
				
			||||||
 | 
									digest.Write(buf[:])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pos > 0 {
 | 
				
			||||||
 | 
							digest.Write(buf[:pos])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return hex.EncodeToString(digest.Sum(nil))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func putInt(buf []byte, n int32) {
 | 
				
			||||||
 | 
						buf[0] = byte(n >> 24 & 0xff)
 | 
				
			||||||
 | 
						buf[1] = byte(n >> 16 & 0xff)
 | 
				
			||||||
 | 
						buf[2] = byte(n >> 8 & 0xff)
 | 
				
			||||||
 | 
						buf[3] = byte(n & 0xff)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								model/token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								model/token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Token struct {
 | 
				
			||||||
 | 
						createAt        int64
 | 
				
			||||||
 | 
						ClientToken     string
 | 
				
			||||||
 | 
						AccessToken     string
 | 
				
			||||||
 | 
						SelectedProfile Profile
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AvailableLevel uint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						Valid AvailableLevel = iota
 | 
				
			||||||
 | 
						NeedRefresh
 | 
				
			||||||
 | 
						Invalid
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewToken(accessToken string, clientToken *string, selectedProfile *Profile) (this Token) {
 | 
				
			||||||
 | 
						this.createAt = time.Now().UnixMilli()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if clientToken == nil || (len(*clientToken) == 0) {
 | 
				
			||||||
 | 
							this.ClientToken = util.RandomUUID()
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							this.ClientToken = *clientToken
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						this.AccessToken = accessToken
 | 
				
			||||||
 | 
						this.SelectedProfile = *selectedProfile
 | 
				
			||||||
 | 
						return this
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Token) GetAvailableLevel() AvailableLevel {
 | 
				
			||||||
 | 
						d := time.Now().Sub(time.UnixMilli(t.createAt))
 | 
				
			||||||
 | 
						if d > time.Hour*24*30 {
 | 
				
			||||||
 | 
							return Invalid
 | 
				
			||||||
 | 
						} else if d > time.Hour*24*15 {
 | 
				
			||||||
 | 
							return NeedRefresh
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return Valid
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										87
									
								
								model/user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								model/user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package model
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type User struct {
 | 
				
			||||||
 | 
						ID                 uuid.UUID `gorm:"column:id;type:bytes;size:36;primaryKey"`
 | 
				
			||||||
 | 
						CreatedAt          time.Time
 | 
				
			||||||
 | 
						UpdatedAt          time.Time
 | 
				
			||||||
 | 
						Email              string   `gorm:"size:64;uniqueIndex:email_idx"`
 | 
				
			||||||
 | 
						Password           string   `gorm:"size:255"`
 | 
				
			||||||
 | 
						ProfileName        string   `gorm:"size:64;uniqueIndex:profile_name_idx"`
 | 
				
			||||||
 | 
						ProfileModelType   string   `gorm:"size:8;default:STEVE"`
 | 
				
			||||||
 | 
						SerializedTextures string   `gorm:"type:TEXT NULL"`
 | 
				
			||||||
 | 
						profile            *Profile `gorm:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *User) Profile() (*Profile, error) {
 | 
				
			||||||
 | 
						if len(u.ProfileName) == 0 {
 | 
				
			||||||
 | 
							return nil, util.NewIllegalArgumentError("Do not have profile")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if u.profile != nil {
 | 
				
			||||||
 | 
							return u.profile, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							var modelType ModelType
 | 
				
			||||||
 | 
							if u.ProfileModelType == "ALEX" {
 | 
				
			||||||
 | 
								modelType = ALEX
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								modelType = STEVE
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							profile := NewProfile(u.ID, u.ProfileName, modelType, u.SerializedTextures)
 | 
				
			||||||
 | 
							return &profile, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *User) SetProfile(p *Profile) {
 | 
				
			||||||
 | 
						u.profile = p
 | 
				
			||||||
 | 
						u.ProfileName = p.Name
 | 
				
			||||||
 | 
						switch p.ModelType {
 | 
				
			||||||
 | 
						case ALEX:
 | 
				
			||||||
 | 
							u.ProfileModelType = "ALEX"
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						case STEVE:
 | 
				
			||||||
 | 
							u.ProfileModelType = "STEVE"
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						serialized, err := json.Marshal(p.Textures)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic("Can not serialize texture")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						u.SerializedTextures = string(serialized)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserResponse struct {
 | 
				
			||||||
 | 
						Username   string                `json:"username,omitempty"`
 | 
				
			||||||
 | 
						Properties []util.StringProperty `json:"properties"`
 | 
				
			||||||
 | 
						Id         string                `json:"id,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *User) ToResponse() UserResponse {
 | 
				
			||||||
 | 
						return UserResponse{
 | 
				
			||||||
 | 
							Id:         util.UnsignedString(u.ID),
 | 
				
			||||||
 | 
							Username:   u.ProfileName,
 | 
				
			||||||
 | 
							Properties: make([]util.StringProperty, 0),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								router/home_router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								router/home_router.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MetaInfo struct {
 | 
				
			||||||
 | 
						ImplementationName    string `json:"implementationName,omitempty"`
 | 
				
			||||||
 | 
						ImplementationVersion string `json:"implementationVersion,omitempty"`
 | 
				
			||||||
 | 
						ServerName            string `json:"serverName,omitempty"`
 | 
				
			||||||
 | 
						Links                 struct {
 | 
				
			||||||
 | 
							Homepage string `json:"homepage,omitempty"`
 | 
				
			||||||
 | 
							Register string `json:"register,omitempty"`
 | 
				
			||||||
 | 
						} `json:"links"`
 | 
				
			||||||
 | 
						FeatureNonEmailLogin     bool `json:"feature.non_email_login,omitempty"`
 | 
				
			||||||
 | 
						FeatureLegacySkinApi     bool `json:"feature.legacy_skin_api,omitempty"`
 | 
				
			||||||
 | 
						FeatureNoMojangNamespace bool `json:"feature.no_mojang_namespace,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ServerMeta struct {
 | 
				
			||||||
 | 
						Meta               MetaInfo `json:"meta"`
 | 
				
			||||||
 | 
						SkinDomains        []string `json:"skinDomains"`
 | 
				
			||||||
 | 
						SignaturePublickey string   `json:"signaturePublickey"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type HomeRouter interface {
 | 
				
			||||||
 | 
						Home(c *gin.Context)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type homeRouterImpl struct {
 | 
				
			||||||
 | 
						serverMeta ServerMeta
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewHomeRouter(meta *ServerMeta) HomeRouter {
 | 
				
			||||||
 | 
						homeRouter := homeRouterImpl{
 | 
				
			||||||
 | 
							serverMeta: *meta,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &homeRouter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Home 首页路由
 | 
				
			||||||
 | 
					func (h *homeRouterImpl) Home(c *gin.Context) {
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, h.serverMeta)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										68
									
								
								router/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								router/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
						"yggdrasil-go/service"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func InitRouters(router *gin.Engine, db *gorm.DB, meta *ServerMeta, skinRootUrl string) {
 | 
				
			||||||
 | 
						err := router.SetTrustedProxies([]string{"127.0.0.1"})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							panic(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokenService := service.NewTokenService()
 | 
				
			||||||
 | 
						userService := service.NewUserService(tokenService, db)
 | 
				
			||||||
 | 
						sessionService := service.NewSessionService(tokenService)
 | 
				
			||||||
 | 
						textureService := service.NewTextureService(tokenService, db)
 | 
				
			||||||
 | 
						homeRouter := NewHomeRouter(meta)
 | 
				
			||||||
 | 
						userRouter := NewUserRouter(userService, skinRootUrl)
 | 
				
			||||||
 | 
						sessionRouter := NewSessionRouter(sessionService, skinRootUrl)
 | 
				
			||||||
 | 
						textureRouter := NewTextureRouter(textureService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router.GET("/", homeRouter.Home)
 | 
				
			||||||
 | 
						authserver := router.Group("/authserver")
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							authserver.POST("/register", userRouter.Register)
 | 
				
			||||||
 | 
							authserver.POST("/authenticate", userRouter.Login)
 | 
				
			||||||
 | 
							authserver.POST("/change", userRouter.ChangeProfile)
 | 
				
			||||||
 | 
							authserver.POST("/refresh", userRouter.Refresh)
 | 
				
			||||||
 | 
							authserver.POST("/validate", userRouter.Validate)
 | 
				
			||||||
 | 
							authserver.POST("/invalidate", userRouter.Invalidate)
 | 
				
			||||||
 | 
							authserver.POST("/signout", userRouter.Signout)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						router.GET("/users/profiles/minecraft/:username", userRouter.UsernameToUUID)
 | 
				
			||||||
 | 
						sessionserver := router.Group("/sessionserver/session/minecraft")
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							sessionserver.GET("/profile/:uuid", userRouter.QueryProfile)
 | 
				
			||||||
 | 
							sessionserver.POST("/join", sessionRouter.JoinServer)
 | 
				
			||||||
 | 
							sessionserver.GET("/hasJoined", sessionRouter.HasJoinedServer)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						router.GET("/textures/:hash", textureRouter.GetTexture)
 | 
				
			||||||
 | 
						api := router.Group("/api")
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							api.POST("/profiles/minecraft", userRouter.QueryUUIDs)
 | 
				
			||||||
 | 
							api.POST("/user/profile/:uuid/:textureType", textureRouter.SetTexture)
 | 
				
			||||||
 | 
							api.PUT("/user/profile/:uuid/:textureType", textureRouter.UploadTexture)
 | 
				
			||||||
 | 
							api.DELETE("/user/profile/:uuid/:textureType", textureRouter.DeleteTexture)
 | 
				
			||||||
 | 
							api.GET("/users/profiles/minecraft/:username", userRouter.UsernameToUUID)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										85
									
								
								router/session_router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								router/session_router.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"yggdrasil-go/service"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SessionRouter interface {
 | 
				
			||||||
 | 
						JoinServer(c *gin.Context)
 | 
				
			||||||
 | 
						HasJoinedServer(c *gin.Context)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sessionRouterImpl struct {
 | 
				
			||||||
 | 
						sessionService service.SessionService
 | 
				
			||||||
 | 
						skinRootUrl    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewSessionRouter(sessionService service.SessionService, skinRootUrl string) SessionRouter {
 | 
				
			||||||
 | 
						sessionRouter := sessionRouterImpl{
 | 
				
			||||||
 | 
							sessionService: sessionService,
 | 
				
			||||||
 | 
							skinRootUrl:    skinRootUrl,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &sessionRouter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type JoinServerRequest struct {
 | 
				
			||||||
 | 
						AccessToken     string `json:"accessToken" binding:"required"`
 | 
				
			||||||
 | 
						SelectedProfile string `json:"selectedProfile" binding:"required"`
 | 
				
			||||||
 | 
						ServerId        string `json:"serverId" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sessionRouterImpl) JoinServer(c *gin.Context) {
 | 
				
			||||||
 | 
						request := JoinServerRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ip := c.Request.RemoteAddr[:strings.LastIndexByte(c.Request.RemoteAddr, ':')]
 | 
				
			||||||
 | 
						err = s.sessionService.JoinServer(request.AccessToken, request.ServerId, request.SelectedProfile, ip)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sessionRouterImpl) HasJoinedServer(c *gin.Context) {
 | 
				
			||||||
 | 
						username := c.Query("username")
 | 
				
			||||||
 | 
						serverId := c.Query("serverId")
 | 
				
			||||||
 | 
						ip := c.DefaultQuery("ip",
 | 
				
			||||||
 | 
							c.Request.RemoteAddr[:strings.LastIndexByte(c.Request.RemoteAddr, ':')])
 | 
				
			||||||
 | 
						var textureBaseUrl string
 | 
				
			||||||
 | 
						if len(s.skinRootUrl) > 0 {
 | 
				
			||||||
 | 
							textureBaseUrl = strings.TrimRight(s.skinRootUrl, "/") + "/textures"
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							textureBaseUrl = c.Request.URL.Scheme + "://" + c.Request.URL.Hostname() + "/textures"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := s.sessionService.HasJoinedServer(serverId, username, ip, textureBaseUrl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										166
									
								
								router/texture_router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								router/texture_router.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/service"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TextureRouter interface {
 | 
				
			||||||
 | 
						GetTexture(c *gin.Context)
 | 
				
			||||||
 | 
						SetTexture(c *gin.Context)
 | 
				
			||||||
 | 
						UploadTexture(c *gin.Context)
 | 
				
			||||||
 | 
						DeleteTexture(c *gin.Context)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type textureRouterImpl struct {
 | 
				
			||||||
 | 
						textureService service.TextureService
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewTextureRouter(textureService service.TextureService) TextureRouter {
 | 
				
			||||||
 | 
						textureRouter := textureRouterImpl{textureService: textureService}
 | 
				
			||||||
 | 
						return &textureRouter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SetTextureRequest struct {
 | 
				
			||||||
 | 
						Url   string `json:"url" binding:"required,url"`
 | 
				
			||||||
 | 
						Model string `json:"model" binding:"oneof=slim default"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureRouterImpl) GetTexture(c *gin.Context) {
 | 
				
			||||||
 | 
						hash := c.Param("hash")
 | 
				
			||||||
 | 
						response, err := t.textureService.GetTexture(hash)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Header("Cache-Control", "public, max-age=31536000")
 | 
				
			||||||
 | 
						c.Data(http.StatusOK, "image/png", response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureRouterImpl) SetTexture(c *gin.Context) {
 | 
				
			||||||
 | 
						request := SetTextureRequest{Model: string(model.STEVE)}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						bearerToken := c.GetHeader("Authorization")
 | 
				
			||||||
 | 
						if len(bearerToken) < 8 {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusUnauthorized, util.NewForbiddenOperationError(util.MessageInvalidToken))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						accessToken := bearerToken[7:]
 | 
				
			||||||
 | 
						profileId, err := util.ToUUID(c.Param("uuid"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						textureType := c.Param("textureType")
 | 
				
			||||||
 | 
						if textureType != "skin" && textureType != "cape" {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError("Invalid texture type."))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if request.Model == "" {
 | 
				
			||||||
 | 
							request.Model = string(model.STEVE)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						modelType := model.ModelType(request.Model)
 | 
				
			||||||
 | 
						err = t.textureService.SetTexture(accessToken, profileId, request.Url, textureType, &modelType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureRouterImpl) UploadTexture(c *gin.Context) {
 | 
				
			||||||
 | 
						bearerToken := c.GetHeader("Authorization")
 | 
				
			||||||
 | 
						if len(bearerToken) < 8 {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusUnauthorized, util.NewForbiddenOperationError(util.MessageInvalidToken))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						accessToken := bearerToken[7:]
 | 
				
			||||||
 | 
						profileId, err := util.ToUUID(c.Param("uuid"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						textureType := c.Param("textureType")
 | 
				
			||||||
 | 
						if textureType != "skin" && textureType != "cape" {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError("Invalid texture type."))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						modelStr := c.PostForm("model")
 | 
				
			||||||
 | 
						modelType := model.STEVE
 | 
				
			||||||
 | 
						if modelStr == "ALEX" {
 | 
				
			||||||
 | 
							modelType = model.ALEX
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						file, err := c.FormFile("file")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if file.Size > (1 << 20) {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError("File too large(more than 1MiB)"))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fileReader, err := file.Open()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusInternalServerError, util.YggdrasilError{
 | 
				
			||||||
 | 
								ErrorCode:    "Internal Server Error",
 | 
				
			||||||
 | 
								ErrorMessage: "Can not open file.",
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer fileReader.Close()
 | 
				
			||||||
 | 
						err = t.textureService.UploadTexture(accessToken, profileId, fileReader, textureType, &modelType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureRouterImpl) DeleteTexture(c *gin.Context) {
 | 
				
			||||||
 | 
						bearerToken := c.GetHeader("Authorization")
 | 
				
			||||||
 | 
						if len(bearerToken) < 8 {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusUnauthorized, util.NewForbiddenOperationError(util.MessageInvalidToken))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						accessToken := bearerToken[7:]
 | 
				
			||||||
 | 
						profileId, err := util.ToUUID(c.Param("uuid"))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						textureType := c.Param("textureType")
 | 
				
			||||||
 | 
						if textureType != "skin" && textureType != "cape" {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError("Invalid texture type."))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = t.textureService.DeleteTexture(accessToken, profileId, textureType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										265
									
								
								router/user_router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								router/user_router.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,265 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/service"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserRouter interface {
 | 
				
			||||||
 | 
						Register(c *gin.Context)
 | 
				
			||||||
 | 
						Login(c *gin.Context)
 | 
				
			||||||
 | 
						ChangeProfile(c *gin.Context)
 | 
				
			||||||
 | 
						Refresh(c *gin.Context)
 | 
				
			||||||
 | 
						Validate(c *gin.Context)
 | 
				
			||||||
 | 
						Invalidate(c *gin.Context)
 | 
				
			||||||
 | 
						Signout(c *gin.Context)
 | 
				
			||||||
 | 
						UsernameToUUID(c *gin.Context)
 | 
				
			||||||
 | 
						QueryUUIDs(c *gin.Context)
 | 
				
			||||||
 | 
						QueryProfile(c *gin.Context)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type userRouterImpl struct {
 | 
				
			||||||
 | 
						userService service.UserService
 | 
				
			||||||
 | 
						skinRootUrl string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewUserRouter(userService service.UserService, skinRootUrl string) UserRouter {
 | 
				
			||||||
 | 
						userRouter := userRouterImpl{
 | 
				
			||||||
 | 
							userService: userService,
 | 
				
			||||||
 | 
							skinRootUrl: skinRootUrl,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &userRouter
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RegRequest struct {
 | 
				
			||||||
 | 
						Username    string `json:"username" binding:"required,email"`
 | 
				
			||||||
 | 
						Password    string `json:"password" binding:"required"`
 | 
				
			||||||
 | 
						ProfileName string `json:"profileName" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MinecraftAgent struct {
 | 
				
			||||||
 | 
						Name    string `json:"name"`
 | 
				
			||||||
 | 
						Version int    `json:"version"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ClientTokenBase struct {
 | 
				
			||||||
 | 
						ClientToken *string `json:"clientToken,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AccessTokenBase struct {
 | 
				
			||||||
 | 
						AccessToken string `json:"accessToken" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DualTokenBase struct {
 | 
				
			||||||
 | 
						AccessTokenBase
 | 
				
			||||||
 | 
						ClientTokenBase
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LoginRequest struct {
 | 
				
			||||||
 | 
						ClientTokenBase
 | 
				
			||||||
 | 
						Username    string          `json:"username" binding:"required,email"`
 | 
				
			||||||
 | 
						Password    string          `json:"password" binding:"required"`
 | 
				
			||||||
 | 
						RequestUser bool            `json:"requestUser"`
 | 
				
			||||||
 | 
						Agent       *MinecraftAgent `json:"agent,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RefreshRequest struct {
 | 
				
			||||||
 | 
						DualTokenBase
 | 
				
			||||||
 | 
						RequestUser     bool                   `json:"requestUser"`
 | 
				
			||||||
 | 
						SelectedProfile *model.ProfileResponse `json:"selectedProfile,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ValidateRequest struct {
 | 
				
			||||||
 | 
						DualTokenBase
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChangeProfileRequest struct {
 | 
				
			||||||
 | 
						DualTokenBase
 | 
				
			||||||
 | 
						ChangeTo string `json:"changeTo" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type InvalidateRequest struct {
 | 
				
			||||||
 | 
						AccessTokenBase
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SignoutRequest struct {
 | 
				
			||||||
 | 
						Username string `json:"username" binding:"required,email"`
 | 
				
			||||||
 | 
						Password string `json:"password" binding:"required"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Register(c *gin.Context) {
 | 
				
			||||||
 | 
						request := RegRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := u.userService.Register(request.Username, request.Password, request.ProfileName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Login(c *gin.Context) {
 | 
				
			||||||
 | 
						request := LoginRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := u.userService.Login(request.Username, request.Password, request.ClientToken, request.RequestUser)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) ChangeProfile(c *gin.Context) {
 | 
				
			||||||
 | 
						request := ChangeProfileRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = u.userService.ChangeProfile(request.AccessToken, request.ClientToken, request.ChangeTo)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Refresh(c *gin.Context) {
 | 
				
			||||||
 | 
						request := RefreshRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := u.userService.Refresh(request.AccessToken, request.ClientToken, request.RequestUser, request.SelectedProfile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Validate(c *gin.Context) {
 | 
				
			||||||
 | 
						request := ValidateRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = u.userService.Validate(request.AccessToken, request.ClientToken)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Invalidate(c *gin.Context) {
 | 
				
			||||||
 | 
						request := InvalidateRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = u.userService.Invalidate(request.AccessToken)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) Signout(c *gin.Context) {
 | 
				
			||||||
 | 
						request := SignoutRequest{}
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = u.userService.Signout(request.Username, request.Password)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) UsernameToUUID(c *gin.Context) {
 | 
				
			||||||
 | 
						username := c.Param("username")
 | 
				
			||||||
 | 
						response, err := u.userService.UsernameToUUID(username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if response != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) QueryUUIDs(c *gin.Context) {
 | 
				
			||||||
 | 
						var request []string
 | 
				
			||||||
 | 
						err := c.ShouldBindJSON(&request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, util.NewForbiddenOperationError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := u.userService.QueryUUIDs(request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userRouterImpl) QueryProfile(c *gin.Context) {
 | 
				
			||||||
 | 
						profileIdStr := c.Param("uuid")
 | 
				
			||||||
 | 
						profileId, err := util.ToUUID(profileIdStr)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusBadRequest, util.NewIllegalArgumentError(err.Error()))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						unsigned := "true" == c.DefaultQuery("unsigned", "false")
 | 
				
			||||||
 | 
						var textureBaseUrl string
 | 
				
			||||||
 | 
						if len(u.skinRootUrl) > 0 {
 | 
				
			||||||
 | 
							textureBaseUrl = strings.TrimRight(u.skinRootUrl, "/") + "/textures"
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							textureBaseUrl = c.Request.URL.Scheme + "://" + c.Request.URL.Hostname() + "/textures"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := u.userService.QueryProfile(profileId, unsigned, textureBaseUrl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							util.HandleError(c, err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, response)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										89
									
								
								service/session_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								service/session_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SessionService interface {
 | 
				
			||||||
 | 
						JoinServer(accessToken string, serverId string, selectedProfile string, ip string) error
 | 
				
			||||||
 | 
						HasJoinedServer(serverId string, username string, ip string, textureBaseUrl string) (map[string]interface{}, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type sessionStore struct {
 | 
				
			||||||
 | 
						sessionCache *lru.Cache
 | 
				
			||||||
 | 
						tokenService TokenService
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewSessionService(service TokenService) SessionService {
 | 
				
			||||||
 | 
						cache, _ := lru.New(100000)
 | 
				
			||||||
 | 
						store := sessionStore{
 | 
				
			||||||
 | 
							sessionCache: cache,
 | 
				
			||||||
 | 
							tokenService: service,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &store
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sessionStore) JoinServer(accessToken string, serverId string, selectedProfile string, ip string) error {
 | 
				
			||||||
 | 
						token, ok := s.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
						if ok && util.UnsignedString(token.SelectedProfile.Id) == selectedProfile {
 | 
				
			||||||
 | 
							session := model.NewAuthenticationSession(serverId, token, ip)
 | 
				
			||||||
 | 
							s.sessionCache.Add(serverId, &session)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]string{
 | 
				
			||||||
 | 
								"accessToken":     accessToken,
 | 
				
			||||||
 | 
								"selectedProfile": selectedProfile,
 | 
				
			||||||
 | 
								"serverId":        serverId,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := util.PostObjectForError("https://sessionserver.mojang.com/session/minecraft/join", data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *sessionStore) HasJoinedServer(serverId string, username string, ip string, textureBaseUrl string) (map[string]interface{}, error) {
 | 
				
			||||||
 | 
						if value, ok := s.sessionCache.Get(serverId); ok {
 | 
				
			||||||
 | 
							if session, ok := value.(*model.AuthenticationSession); ok {
 | 
				
			||||||
 | 
								if !(session.HasExpired() && s.sessionCache.Remove(serverId)) &&
 | 
				
			||||||
 | 
									(ip == session.Ip) && (session.Token.SelectedProfile.Name == username) {
 | 
				
			||||||
 | 
									return session.Token.SelectedProfile.ToCompleteResponse(true, textureBaseUrl)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							m := make(map[string]interface{})
 | 
				
			||||||
 | 
							includeIp := ""
 | 
				
			||||||
 | 
							if ip != "" {
 | 
				
			||||||
 | 
								includeIp = "&ip=" + url.QueryEscape(ip)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := util.GetObject(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s%s", url.QueryEscape(username), url.QueryEscape(serverId), includeIp), &m)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return m, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, util.YggdrasilError{Status: http.StatusNoContent}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										246
									
								
								service/texture_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								service/texture_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,246 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
						"image"
 | 
				
			||||||
 | 
						_ "image/jpeg"
 | 
				
			||||||
 | 
						"image/png"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TextureService interface {
 | 
				
			||||||
 | 
						GetTexture(hash string) ([]byte, error)
 | 
				
			||||||
 | 
						SetTexture(accessToken string, profileId uuid.UUID, skinUrl string, textureType string, model *model.ModelType) error
 | 
				
			||||||
 | 
						UploadTexture(accessToken string, profileId uuid.UUID, skinReader io.Reader, textureType string, model *model.ModelType) error
 | 
				
			||||||
 | 
						DeleteTexture(accessToken string, profileId uuid.UUID, textureType string) error
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type textureServiceImpl struct {
 | 
				
			||||||
 | 
						tokenService TokenService
 | 
				
			||||||
 | 
						db           *gorm.DB
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewTextureService(tokenService TokenService, db *gorm.DB) TextureService {
 | 
				
			||||||
 | 
						textureService := textureServiceImpl{
 | 
				
			||||||
 | 
							tokenService: tokenService,
 | 
				
			||||||
 | 
							db:           db,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &textureService
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureServiceImpl) GetTexture(hash string) ([]byte, error) {
 | 
				
			||||||
 | 
						texture := model.Texture{}
 | 
				
			||||||
 | 
						if err := t.db.First(&texture, "hash = ?", hash).Error; err == nil {
 | 
				
			||||||
 | 
							return texture.Data, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							err := util.YggdrasilError{
 | 
				
			||||||
 | 
								Status:       http.StatusNotFound,
 | 
				
			||||||
 | 
								ErrorCode:    "Not Found",
 | 
				
			||||||
 | 
								ErrorMessage: "Texture Not Found",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil, &err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureServiceImpl) SetTexture(accessToken string, profileId uuid.UUID, skinUrl string, textureType string, modelType *model.ModelType) error {
 | 
				
			||||||
 | 
						token, ok := t.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
						if !ok || token.GetAvailableLevel() != model.Valid {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if token.SelectedProfile.Id != profileId {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("Profile mismatch.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := t.db.First(&user, profileId).Error; err != nil {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						skinDownloadUrl, err := url.Parse(skinUrl)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return util.NewIllegalArgumentError("Invalid skin url: " + err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response, err := http.Get(skinDownloadUrl.String())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer response.Body.Close()
 | 
				
			||||||
 | 
						if response.ContentLength > 1048576 {
 | 
				
			||||||
 | 
							return util.NewIllegalArgumentError("File too large(more than 1MiB)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						reader := io.LimitReader(response.Body, 1048576)
 | 
				
			||||||
 | 
						var header bytes.Buffer
 | 
				
			||||||
 | 
						conf, _, err := image.DecodeConfig(io.TeeReader(reader, &header))
 | 
				
			||||||
 | 
						if err != nil || conf.Width > 1024 || conf.Height > 1024 {
 | 
				
			||||||
 | 
							return util.NewIllegalArgumentError("Image too large(max 1024 pixels each dimension)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						im, _, err := image.Decode(io.MultiReader(&header, reader))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = t.saveTexture(&user, im, textureType, modelType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							profile, _ := user.Profile()
 | 
				
			||||||
 | 
							token.SelectedProfile = *profile
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureServiceImpl) UploadTexture(accessToken string, profileId uuid.UUID, skinReader io.Reader, textureType string, modelType *model.ModelType) error {
 | 
				
			||||||
 | 
						token, ok := t.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
						if !ok || token.GetAvailableLevel() != model.Valid {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if token.SelectedProfile.Id != profileId {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("Profile mismatch.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := t.db.First(&user, profileId).Error; err != nil {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						reader := io.LimitReader(skinReader, 1048576)
 | 
				
			||||||
 | 
						var header bytes.Buffer
 | 
				
			||||||
 | 
						conf, _, err := image.DecodeConfig(io.TeeReader(reader, &header))
 | 
				
			||||||
 | 
						if err != nil || conf.Width > 1024 || conf.Height > 1024 {
 | 
				
			||||||
 | 
							return util.NewIllegalArgumentError("Image too large(max 1024 pixels each dimension)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						im, _, err := image.Decode(io.MultiReader(&header, reader))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = t.saveTexture(&user, im, textureType, modelType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							profile, _ := user.Profile()
 | 
				
			||||||
 | 
							token.SelectedProfile = *profile
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureServiceImpl) DeleteTexture(accessToken string, profileId uuid.UUID, textureType string) error {
 | 
				
			||||||
 | 
						token, ok := t.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
						if !ok || token.GetAvailableLevel() != model.Valid {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if token.SelectedProfile.Id != profileId {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("Profile mismatch.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := t.db.First(&user, profileId).Error; err != nil {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						textureType = strings.ToUpper(textureType)
 | 
				
			||||||
 | 
						if textureType != "SKIN" && textureType != "CAPE" {
 | 
				
			||||||
 | 
							textureType = "SKIN"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var profile *model.Profile
 | 
				
			||||||
 | 
						hash, ok := token.SelectedProfile.Textures[textureType]
 | 
				
			||||||
 | 
						if ok {
 | 
				
			||||||
 | 
							delete(token.SelectedProfile.Textures, textureType)
 | 
				
			||||||
 | 
							profile = &token.SelectedProfile
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							p, err := user.Profile()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							hash, ok = p.Textures[textureType]
 | 
				
			||||||
 | 
							if ok {
 | 
				
			||||||
 | 
								delete(p.Textures, textureType)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return util.NewForbiddenOperationError(util.MessageProfileNotFound)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							profile = p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							texture := model.Texture{}
 | 
				
			||||||
 | 
							if err := tx.Select("hash", "used").First(&texture, "hash = ?", hash).Error; err == nil {
 | 
				
			||||||
 | 
								if texture.Used < 2 {
 | 
				
			||||||
 | 
									tx.Delete(&texture)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									tx.Model(&texture).Update("used", gorm.Expr("used - ?", 1))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							user.SetProfile(profile)
 | 
				
			||||||
 | 
							return tx.Save(&user).Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *textureServiceImpl) saveTexture(user *model.User, skinImage image.Image, textureType string, modelType *model.ModelType) error {
 | 
				
			||||||
 | 
						var modelValue model.ModelType
 | 
				
			||||||
 | 
						if modelType != nil && *modelType == model.ALEX {
 | 
				
			||||||
 | 
							modelValue = *modelType
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							modelValue = model.STEVE
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						textureType = strings.ToUpper(textureType)
 | 
				
			||||||
 | 
						if textureType != "SKIN" && textureType != "CAPE" {
 | 
				
			||||||
 | 
							textureType = "SKIN"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t.db.Transaction(func(tx *gorm.DB) error {
 | 
				
			||||||
 | 
							profile, err := user.Profile()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if textureType == "SKIN" {
 | 
				
			||||||
 | 
								profile.ModelType = modelValue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							hash := model.ComputeTextureId(skinImage)
 | 
				
			||||||
 | 
							oldHash, oldExist := profile.Textures[textureType]
 | 
				
			||||||
 | 
							texture := model.Texture{}
 | 
				
			||||||
 | 
							if err := tx.First(&texture, "hash = ?", hash).Error; err != nil {
 | 
				
			||||||
 | 
								texture.Hash = hash
 | 
				
			||||||
 | 
								texture.Used = 1
 | 
				
			||||||
 | 
								buffer := bytes.Buffer{}
 | 
				
			||||||
 | 
								err := png.Encode(&buffer, skinImage)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								texture.Data = buffer.Bytes()
 | 
				
			||||||
 | 
								if err := tx.Create(&texture).Error; err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								if oldExist && oldHash != hash {
 | 
				
			||||||
 | 
									tx.Model(&texture).Update("used", gorm.Expr("used + ?", 1))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if oldExist && oldHash != hash {
 | 
				
			||||||
 | 
								oldTexture := model.Texture{}
 | 
				
			||||||
 | 
								if err := tx.Select("hash", "used").First(&oldTexture, "hash = ?", oldHash).Error; err == nil {
 | 
				
			||||||
 | 
									if oldTexture.Used < 2 {
 | 
				
			||||||
 | 
										tx.Delete(&oldTexture)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										tx.Model(&oldTexture).Update("used", gorm.Expr("used - ?", 1))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							profile.Textures[textureType] = hash
 | 
				
			||||||
 | 
							user.SetProfile(profile)
 | 
				
			||||||
 | 
							return tx.Save(&user).Error
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										114
									
								
								service/token_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								service/token_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TokenService interface {
 | 
				
			||||||
 | 
						RemoveToken(token *model.Token)
 | 
				
			||||||
 | 
						RemoveAccessToken(accessToken string)
 | 
				
			||||||
 | 
						RemoveAll(profileId uuid.UUID)
 | 
				
			||||||
 | 
						AcquireToken(user *model.User, clientToken *string, profile *model.Profile) *model.Token
 | 
				
			||||||
 | 
						VerifyToken(accessToken string, clientToken *string) model.AvailableLevel
 | 
				
			||||||
 | 
						GetToken(accessToken string) (*model.Token, bool)
 | 
				
			||||||
 | 
						UpdateProfile(profileId uuid.UUID, profile *model.Profile)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type tokenStore struct {
 | 
				
			||||||
 | 
						tokenCache *lru.Cache
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewTokenService() TokenService {
 | 
				
			||||||
 | 
						cache, _ := lru.New(10000000)
 | 
				
			||||||
 | 
						store := tokenStore{
 | 
				
			||||||
 | 
							tokenCache: cache,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &store
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) RemoveToken(token *model.Token) {
 | 
				
			||||||
 | 
						t.RemoveAccessToken(token.AccessToken)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) RemoveAccessToken(accessToken string) {
 | 
				
			||||||
 | 
						t.tokenCache.Remove(accessToken)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) RemoveAll(profileId uuid.UUID) {
 | 
				
			||||||
 | 
						keys := t.tokenCache.Keys()
 | 
				
			||||||
 | 
						for _, k := range keys {
 | 
				
			||||||
 | 
							if v, ok := t.tokenCache.Get(k); ok {
 | 
				
			||||||
 | 
								if v.(*model.Token).SelectedProfile.Id == profileId {
 | 
				
			||||||
 | 
									t.tokenCache.Remove(k)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) AcquireToken(user *model.User, clientToken *string, profile *model.Profile) *model.Token {
 | 
				
			||||||
 | 
						if profile == nil {
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							profile, err = user.Profile()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								panic(err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						token := model.NewToken(util.RandomUUID(), clientToken, profile)
 | 
				
			||||||
 | 
						t.tokenCache.Add(token.AccessToken, &token)
 | 
				
			||||||
 | 
						return &token
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) VerifyToken(accessToken string, clientToken *string) model.AvailableLevel {
 | 
				
			||||||
 | 
						if value, ok := t.tokenCache.Get(accessToken); ok {
 | 
				
			||||||
 | 
							if token, ok := value.(*model.Token); ok {
 | 
				
			||||||
 | 
								if clientToken != nil && token.ClientToken != *clientToken {
 | 
				
			||||||
 | 
									return model.Invalid
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if token.GetAvailableLevel() == model.Invalid {
 | 
				
			||||||
 | 
									t.RemoveToken(token)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return token.GetAvailableLevel()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return model.Invalid
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) GetToken(accessToken string) (*model.Token, bool) {
 | 
				
			||||||
 | 
						if value, ok := t.tokenCache.Get(accessToken); ok {
 | 
				
			||||||
 | 
							if token, ok := value.(*model.Token); ok {
 | 
				
			||||||
 | 
								return token, true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil, false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *tokenStore) UpdateProfile(profileId uuid.UUID, profile *model.Profile) {
 | 
				
			||||||
 | 
						keys := t.tokenCache.Keys()
 | 
				
			||||||
 | 
						for _, k := range keys {
 | 
				
			||||||
 | 
							if v, ok := t.tokenCache.Get(k); ok {
 | 
				
			||||||
 | 
								if token := v.(*model.Token); token.SelectedProfile.Id == profileId {
 | 
				
			||||||
 | 
									token.SelectedProfile = *profile
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										412
									
								
								service/user_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								service/user_service.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,412 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
						lru "github.com/hashicorp/golang-lru"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
 | 
						"golang.org/x/time/rate"
 | 
				
			||||||
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"yggdrasil-go/model"
 | 
				
			||||||
 | 
						"yggdrasil-go/util"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UserService interface {
 | 
				
			||||||
 | 
						Register(username string, password string, profileName string) (*model.UserResponse, error)
 | 
				
			||||||
 | 
						Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error)
 | 
				
			||||||
 | 
						ChangeProfile(accessToken string, clientToken *string, changeTo string) error
 | 
				
			||||||
 | 
						Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error)
 | 
				
			||||||
 | 
						Validate(accessToken string, clientToken *string) error
 | 
				
			||||||
 | 
						Invalidate(accessToken string) error
 | 
				
			||||||
 | 
						Signout(username string, password string) error
 | 
				
			||||||
 | 
						UsernameToUUID(username string) (*model.ProfileResponse, error)
 | 
				
			||||||
 | 
						QueryUUIDs(usernames []string) ([]model.ProfileResponse, error)
 | 
				
			||||||
 | 
						QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LoginResponse struct {
 | 
				
			||||||
 | 
						User              *model.UserResponse     `json:"user"`
 | 
				
			||||||
 | 
						ClientToken       string                  `json:"clientToken"`
 | 
				
			||||||
 | 
						AccessToken       string                  `json:"accessToken"`
 | 
				
			||||||
 | 
						AvailableProfiles []model.ProfileResponse `json:"availableProfiles,omitempty"`
 | 
				
			||||||
 | 
						SelectedProfile   *model.ProfileResponse  `json:"selectedProfile"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type userSrviceImpl struct {
 | 
				
			||||||
 | 
						tokenService  TokenService
 | 
				
			||||||
 | 
						db            *gorm.DB
 | 
				
			||||||
 | 
						limitLruCache *lru.Cache
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewUserService(tokenService TokenService, db *gorm.DB) UserService {
 | 
				
			||||||
 | 
						cache, _ := lru.New(10000)
 | 
				
			||||||
 | 
						userSrvice := userSrviceImpl{
 | 
				
			||||||
 | 
							tokenService:  tokenService,
 | 
				
			||||||
 | 
							db:            db,
 | 
				
			||||||
 | 
							limitLruCache: cache,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &userSrvice
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Register(username string, password string, profileName string) (*model.UserResponse, error) {
 | 
				
			||||||
 | 
						var count int64
 | 
				
			||||||
 | 
						if err := u.db.Table("users").Where("email = ?", username).Count(&count).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count > 0 {
 | 
				
			||||||
 | 
							return nil, util.NewForbiddenOperationError("email exist")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := u.db.Table("users").Where("profile_name = ?", profileName).Count(&count).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count > 0 {
 | 
				
			||||||
 | 
							return nil, util.NewForbiddenOperationError("profileName exist")
 | 
				
			||||||
 | 
						} else if _, err := mojangUsernameToUUID(profileName); err == nil {
 | 
				
			||||||
 | 
							return nil, util.NewForbiddenOperationError("profileName duplicate")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						matched, err := regexp.MatchString("^(\\w){3,}(\\.\\w+)*@(\\w){2,}((\\.\\w+)+)$", username)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !matched || len(password) < 6 || isInvalidProfileName(profileName) {
 | 
				
			||||||
 | 
							return nil, util.NewIllegalArgumentError("bad format(valid email, password longer than 5, profileName longer than 1)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						hashedPass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{
 | 
				
			||||||
 | 
							ID:       uuid.New(),
 | 
				
			||||||
 | 
							Email:    username,
 | 
				
			||||||
 | 
							Password: string(hashedPass),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						profile := model.NewProfile(user.ID, profileName, model.STEVE, "")
 | 
				
			||||||
 | 
						user.SetProfile(&profile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := u.db.Create(&user).Error; err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						response := user.ToResponse()
 | 
				
			||||||
 | 
						return &response, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func isInvalidProfileName(name string) bool {
 | 
				
			||||||
 | 
						// To support Unicode (like Chinese) profile name, abandoned treatment.
 | 
				
			||||||
 | 
						return name == "" || strings.ContainsRune(name, ' ') || len(name) <= 1
 | 
				
			||||||
 | 
						//return name == "" || !name.matches("^[0-1a-zA-Z_]{2,16}$");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error) {
 | 
				
			||||||
 | 
						if !u.allowUser(username) {
 | 
				
			||||||
 | 
							return nil, util.YggdrasilError{
 | 
				
			||||||
 | 
								Status:       http.StatusTooManyRequests,
 | 
				
			||||||
 | 
								ErrorCode:    "ForbiddenOperationException",
 | 
				
			||||||
 | 
								ErrorMessage: "Forbidden",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := u.db.Where("email = ?", username).First(&user).Error; err == nil {
 | 
				
			||||||
 | 
							if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil {
 | 
				
			||||||
 | 
								var useClientToken string
 | 
				
			||||||
 | 
								if clientToken == nil || *clientToken == "" {
 | 
				
			||||||
 | 
									useClientToken = util.RandomUUID()
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									useClientToken = *clientToken
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								token := u.tokenService.AcquireToken(&user, &useClientToken, nil)
 | 
				
			||||||
 | 
								profile, err := user.Profile()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									panic(err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								simpleResponse := profile.ToSimpleResponse()
 | 
				
			||||||
 | 
								var response = LoginResponse{
 | 
				
			||||||
 | 
									AccessToken:       token.AccessToken,
 | 
				
			||||||
 | 
									ClientToken:       token.ClientToken,
 | 
				
			||||||
 | 
									AvailableProfiles: []model.ProfileResponse{simpleResponse},
 | 
				
			||||||
 | 
									SelectedProfile:   &simpleResponse,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								userResponse := user.ToResponse()
 | 
				
			||||||
 | 
								if requestUser {
 | 
				
			||||||
 | 
									response.User = &userResponse
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return &response, nil
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil, util.NewForbiddenOperationError(util.MessageInvalidCredentials)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]interface{}{
 | 
				
			||||||
 | 
								"agent": map[string]interface{}{
 | 
				
			||||||
 | 
									"name":    "Minecraft",
 | 
				
			||||||
 | 
									"version": 1,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"username":    username,
 | 
				
			||||||
 | 
								"password":    password,
 | 
				
			||||||
 | 
								"clientToken": password,
 | 
				
			||||||
 | 
								"requestUser": requestUser,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							loginResponse := LoginResponse{}
 | 
				
			||||||
 | 
							err := util.PostObject("https://authserver.mojang.com/authenticate", data, &loginResponse)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return &loginResponse, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) ChangeProfile(accessToken string, clientToken *string, changeTo string) error {
 | 
				
			||||||
 | 
						if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						token, ok := u.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						profile := token.SelectedProfile
 | 
				
			||||||
 | 
						err := u.db.First(&user, profile.Id).Error
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("User not found")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var count int64
 | 
				
			||||||
 | 
						if err := u.db.Table("users").Where("profile_name = ?", changeTo).Count(&count).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if count > 0 {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("profileName exist")
 | 
				
			||||||
 | 
						} else if _, err := mojangUsernameToUUID(changeTo); err == nil {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("profileName duplicate")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if isInvalidProfileName(changeTo) {
 | 
				
			||||||
 | 
							return util.NewForbiddenOperationError("bad format(profileName longer than 1)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = u.db.Model(&user).Update("profile_name", changeTo).Error; err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						profile.Name = changeTo
 | 
				
			||||||
 | 
						u.tokenService.UpdateProfile(user.ID, &profile)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error) {
 | 
				
			||||||
 | 
						if len(accessToken) <= 36 {
 | 
				
			||||||
 | 
							user := model.User{}
 | 
				
			||||||
 | 
							if selectedProfile != nil {
 | 
				
			||||||
 | 
								// 由于当前实现把用户 UUID 作为角色 UUID,所以不支持角色选择,只要选择了就会报错
 | 
				
			||||||
 | 
								return nil, util.NewForbiddenOperationError(util.MessageTokenAlreadyAssigned)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if u.tokenService.VerifyToken(accessToken, clientToken) == model.Invalid {
 | 
				
			||||||
 | 
								return nil, util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							token, ok := u.tokenService.GetToken(accessToken)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return nil, util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err := u.db.First(&user, token.SelectedProfile.Id).Error; err != nil {
 | 
				
			||||||
 | 
								return nil, util.NewIllegalArgumentError(util.MessageProfileNotFound)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							newToken := u.tokenService.AcquireToken(&user, clientToken, nil)
 | 
				
			||||||
 | 
							u.tokenService.RemoveAccessToken(accessToken)
 | 
				
			||||||
 | 
							simpleResponse := newToken.SelectedProfile.ToSimpleResponse()
 | 
				
			||||||
 | 
							var response = LoginResponse{
 | 
				
			||||||
 | 
								AccessToken:       newToken.AccessToken,
 | 
				
			||||||
 | 
								ClientToken:       newToken.ClientToken,
 | 
				
			||||||
 | 
								AvailableProfiles: []model.ProfileResponse{},
 | 
				
			||||||
 | 
								SelectedProfile:   &simpleResponse,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							userResponse := user.ToResponse()
 | 
				
			||||||
 | 
							if requestUser {
 | 
				
			||||||
 | 
								response.User = &userResponse
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return &response, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]interface{}{
 | 
				
			||||||
 | 
								"accessToken":     accessToken,
 | 
				
			||||||
 | 
								"clientToken":     clientToken,
 | 
				
			||||||
 | 
								"requestUser":     requestUser,
 | 
				
			||||||
 | 
								"selectedProfile": selectedProfile,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							loginResponse := LoginResponse{}
 | 
				
			||||||
 | 
							err := util.PostObject("https://authserver.mojang.com/refresh", data, &loginResponse)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return &loginResponse, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Validate(accessToken string, clientToken *string) error {
 | 
				
			||||||
 | 
						if len(accessToken) <= 36 {
 | 
				
			||||||
 | 
							if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
 | 
				
			||||||
 | 
								return util.NewForbiddenOperationError(util.MessageInvalidToken)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]interface{}{
 | 
				
			||||||
 | 
								"accessToken": accessToken,
 | 
				
			||||||
 | 
								"clientToken": clientToken,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := util.PostObjectForError("https://authserver.mojang.com/validate", data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Invalidate(accessToken string) error {
 | 
				
			||||||
 | 
						if len(accessToken) <= 36 {
 | 
				
			||||||
 | 
							u.tokenService.RemoveAccessToken(accessToken)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]interface{}{
 | 
				
			||||||
 | 
								"accessToken": accessToken,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := util.PostObjectForError("https://authserver.mojang.com/invalidate", data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) Signout(username string, password string) error {
 | 
				
			||||||
 | 
						if !u.allowUser(username) {
 | 
				
			||||||
 | 
							return util.YggdrasilError{
 | 
				
			||||||
 | 
								Status:       http.StatusTooManyRequests,
 | 
				
			||||||
 | 
								ErrorCode:    "ForbiddenOperationException",
 | 
				
			||||||
 | 
								ErrorMessage: "Forbidden",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := u.db.Where("email = ?", username).First(&user).Error; err == nil {
 | 
				
			||||||
 | 
							if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil {
 | 
				
			||||||
 | 
								u.tokenService.RemoveAll(user.ID)
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return util.NewForbiddenOperationError(util.MessageInvalidCredentials)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							data := map[string]interface{}{
 | 
				
			||||||
 | 
								"username": username,
 | 
				
			||||||
 | 
								"password": password,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							err := util.PostObjectForError("https://authserver.mojang.com/signout", data)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) UsernameToUUID(username string) (*model.ProfileResponse, error) {
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if result := u.db.Where("profile_name = ?", username).First(&user); result.Error == nil {
 | 
				
			||||||
 | 
							return &model.ProfileResponse{
 | 
				
			||||||
 | 
								Name: user.ProfileName,
 | 
				
			||||||
 | 
								Id:   util.UnsignedString(user.ID),
 | 
				
			||||||
 | 
							}, nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							response, err := mojangUsernameToUUID(username)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, nil
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return &response, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) QueryUUIDs(usernames []string) ([]model.ProfileResponse, error) {
 | 
				
			||||||
 | 
						var users []model.User
 | 
				
			||||||
 | 
						var names []string
 | 
				
			||||||
 | 
						if len(usernames) > 10 {
 | 
				
			||||||
 | 
							names = usernames[:10]
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							names = usernames
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var responses = make([]model.ProfileResponse, 0)
 | 
				
			||||||
 | 
						if err := u.db.Table("users").Where("profile_name in ?", names).Find(&users).Error; err == nil {
 | 
				
			||||||
 | 
							for _, user := range users {
 | 
				
			||||||
 | 
								responses = append(responses, model.ProfileResponse{
 | 
				
			||||||
 | 
									Name: user.ProfileName,
 | 
				
			||||||
 | 
									Id:   util.UnsignedString(user.ID),
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return responses, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error) {
 | 
				
			||||||
 | 
						user := model.User{}
 | 
				
			||||||
 | 
						if err := u.db.First(&user, profileId).Error; err == nil {
 | 
				
			||||||
 | 
							profile, err := user.Profile()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							response, err := profile.ToCompleteResponse(!unsigned, textureBaseUrl)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return response, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							result := map[string]interface{}{}
 | 
				
			||||||
 | 
							err := util.GetObject(fmt.Sprintf("https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=%t", util.UnsignedString(profileId), unsigned), &result)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return result, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *userSrviceImpl) allowUser(username string) bool {
 | 
				
			||||||
 | 
						if value, ok := u.limitLruCache.Get(username); ok {
 | 
				
			||||||
 | 
							if limiter, ok := value.(*rate.Limiter); ok {
 | 
				
			||||||
 | 
								return limiter.Allow()
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								u.limitLruCache.Remove(username)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							limiter := rate.NewLimiter(0.2, 3)
 | 
				
			||||||
 | 
							u.limitLruCache.Add(username, limiter)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func mojangUsernameToUUID(username string) (model.ProfileResponse, error) {
 | 
				
			||||||
 | 
						response := model.ProfileResponse{}
 | 
				
			||||||
 | 
						reqUrl := fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", url.PathEscape(username))
 | 
				
			||||||
 | 
						err := util.GetObject(reqUrl, &response)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return response, err
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return response, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										82
									
								
								util/error_utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								util/error_utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var MessageInvalidToken = "Invalid token."
 | 
				
			||||||
 | 
					var MessageInvalidCredentials = "Invalid credentials. Invalid username or password."
 | 
				
			||||||
 | 
					var MessageTokenAlreadyAssigned = "Access token already has a profile assigned."
 | 
				
			||||||
 | 
					var MessageAccessDenied = "Access denied."
 | 
				
			||||||
 | 
					var MessageProfileNotFound = "No such profile."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type YggdrasilError struct {
 | 
				
			||||||
 | 
						ErrorCode    string `json:"error"`
 | 
				
			||||||
 | 
						ErrorMessage string `json:"errorMessage"`
 | 
				
			||||||
 | 
						Cause        string `json:"cause,omitempty"`
 | 
				
			||||||
 | 
						Status       int    `json:"-"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (e YggdrasilError) Error() string {
 | 
				
			||||||
 | 
						return e.ErrorMessage
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewIllegalArgumentError(msg string) (err YggdrasilError) {
 | 
				
			||||||
 | 
						err.ErrorCode = "IllegalArgumentException"
 | 
				
			||||||
 | 
						err.Status = http.StatusBadRequest
 | 
				
			||||||
 | 
						err.ErrorMessage = msg
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewForbiddenOperationError(msg string) (err YggdrasilError) {
 | 
				
			||||||
 | 
						err.ErrorCode = "ForbiddenOperationException"
 | 
				
			||||||
 | 
						err.Status = http.StatusForbidden
 | 
				
			||||||
 | 
						err.ErrorMessage = msg
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func HandleError(c *gin.Context, err error) {
 | 
				
			||||||
 | 
						switch x := err.(type) {
 | 
				
			||||||
 | 
						case YggdrasilError:
 | 
				
			||||||
 | 
							if x.Status == 0 {
 | 
				
			||||||
 | 
								x.Status = http.StatusForbidden
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if x.Status == http.StatusNoContent {
 | 
				
			||||||
 | 
								c.Status(x.Status)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								c.AbortWithStatusJSON(x.Status, x)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						case *YggdrasilError:
 | 
				
			||||||
 | 
							if x.Status == 0 {
 | 
				
			||||||
 | 
								x.Status = http.StatusForbidden
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if x.Status == http.StatusNoContent {
 | 
				
			||||||
 | 
								c.Status(x.Status)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								c.AbortWithStatusJSON(x.Status, x)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							c.AbortWithStatusJSON(http.StatusForbidden, x.Error())
 | 
				
			||||||
 | 
							break
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										165
									
								
								util/http_utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								util/http_utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetObject(url string, value interface{}) error {
 | 
				
			||||||
 | 
						resp, err := http.Get(url)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode == http.StatusNoContent {
 | 
				
			||||||
 | 
							return &YggdrasilError{
 | 
				
			||||||
 | 
								Status:       http.StatusNoContent,
 | 
				
			||||||
 | 
								ErrorCode:    "IllegalArgumentException",
 | 
				
			||||||
 | 
								ErrorMessage: "Http No Content",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if resp.StatusCode/100 == 4 {
 | 
				
			||||||
 | 
							decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
							errResp := YggdrasilError{}
 | 
				
			||||||
 | 
							err = decoder.Decode(&errResp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return errResp
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							body, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return json.Unmarshal(body, value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetForString(url string) (string, error) {
 | 
				
			||||||
 | 
						resp, err := http.Get(url)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode == http.StatusNoContent {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						} else if resp.StatusCode/100 == 4 {
 | 
				
			||||||
 | 
							decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
							errResp := YggdrasilError{}
 | 
				
			||||||
 | 
							err = decoder.Decode(&errResp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", errResp
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							body, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return string(body), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func PostObject(url string, data interface{}, result interface{}) error {
 | 
				
			||||||
 | 
						buf := bytes.Buffer{}
 | 
				
			||||||
 | 
						encoder := json.NewEncoder(&buf)
 | 
				
			||||||
 | 
						err := encoder.Encode(data)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := http.Post(url, "application/json", &buf)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode == http.StatusNoContent {
 | 
				
			||||||
 | 
							return &YggdrasilError{
 | 
				
			||||||
 | 
								Status:       http.StatusNoContent,
 | 
				
			||||||
 | 
								ErrorCode:    "IllegalArgumentException",
 | 
				
			||||||
 | 
								ErrorMessage: "Http No Content",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else if resp.StatusCode/100 == 4 {
 | 
				
			||||||
 | 
							decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
							errResp := YggdrasilError{}
 | 
				
			||||||
 | 
							err = decoder.Decode(&errResp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return errResp
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							body, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return json.Unmarshal(body, result)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func PostObjectForError(url string, data interface{}) error {
 | 
				
			||||||
 | 
						buf := bytes.Buffer{}
 | 
				
			||||||
 | 
						encoder := json.NewEncoder(&buf)
 | 
				
			||||||
 | 
						err := encoder.Encode(data)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						resp, err := http.Post(url, "application/json", &buf)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode == http.StatusNoContent {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
							errResp := YggdrasilError{}
 | 
				
			||||||
 | 
							err = decoder.Decode(&errResp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return errResp
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func PostForString(url string, data []byte) (string, error) {
 | 
				
			||||||
 | 
						reader := bytes.NewReader(data)
 | 
				
			||||||
 | 
						resp, err := http.Post(url, "application/json", reader)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
						if resp.StatusCode == http.StatusNoContent {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						} else if resp.StatusCode/100 == 4 {
 | 
				
			||||||
 | 
							decoder := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
							errResp := YggdrasilError{}
 | 
				
			||||||
 | 
							err = decoder.Decode(&errResp)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "", errResp
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							body, err := ioutil.ReadAll(resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return string(body), nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										84
									
								
								util/properties_utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								util/properties_utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto"
 | 
				
			||||||
 | 
						"crypto/rand"
 | 
				
			||||||
 | 
						"crypto/rsa"
 | 
				
			||||||
 | 
						"crypto/sha1"
 | 
				
			||||||
 | 
						"encoding/base64"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Property struct {
 | 
				
			||||||
 | 
						Name  string
 | 
				
			||||||
 | 
						Value interface{}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type StringProperty struct {
 | 
				
			||||||
 | 
						Name  string `json:"name,omitempty"`
 | 
				
			||||||
 | 
						Value string `json:"value,omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PrivateKey RSA PKCS8 Private Key
 | 
				
			||||||
 | 
					var PrivateKey *rsa.PrivateKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func EncodeBase64(properties ...Property) (string, error) {
 | 
				
			||||||
 | 
						obj := make(map[string]interface{})
 | 
				
			||||||
 | 
						for _, property := range properties {
 | 
				
			||||||
 | 
							obj[property.Name] = property.Value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						jsonBytes, err := json.Marshal(obj)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return base64.StdEncoding.EncodeToString(jsonBytes), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Properties(sign bool, properties ...StringProperty) []map[string]string {
 | 
				
			||||||
 | 
						list := make([]map[string]string, 0, len(properties))
 | 
				
			||||||
 | 
						for _, property := range properties {
 | 
				
			||||||
 | 
							obj := map[string]string{
 | 
				
			||||||
 | 
								"name":  property.Name,
 | 
				
			||||||
 | 
								"value": property.Value,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if sign {
 | 
				
			||||||
 | 
								err := error(nil)
 | 
				
			||||||
 | 
								obj["signature"], err = Sign(property.Value)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									log.Printf("无法签名字符串 '%s', 原因: %s", property.Value, err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							list = append(list, obj)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return list
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Sign(value string) (string, error) {
 | 
				
			||||||
 | 
						if PrivateKey == nil {
 | 
				
			||||||
 | 
							panic("未初始化私钥")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sum := sha1.Sum([]byte(value))
 | 
				
			||||||
 | 
						sig, err := rsa.SignPKCS1v15(rand.Reader, PrivateKey, crypto.SHA1, sum[:])
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return base64.StdEncoding.EncodeToString(sig), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								util/uuid_utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								util/uuid_utils.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright (C) 2022. Gardel <sunxinao@hotmail.com> and contributors
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package util
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"github.com/google/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func UnsignedString(u uuid.UUID) string {
 | 
				
			||||||
 | 
						var buf [32]byte
 | 
				
			||||||
 | 
						hex.Encode(buf[:], u[:])
 | 
				
			||||||
 | 
						return string(buf[:])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ToUUID(str string) (uuid.UUID, error) {
 | 
				
			||||||
 | 
						return uuid.Parse(str)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func RandomUUID() string {
 | 
				
			||||||
 | 
						return UnsignedString(uuid.New())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user