Compare commits
147 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1136738111 | ||
|
155750b746 | ||
|
e766e951a5 | ||
|
8a3dae0592 | ||
|
ace4c6bee0 | ||
|
330b90b172 | ||
|
b2c3ada7c0 | ||
|
cc90105130 | ||
|
622658f5aa | ||
|
120c027fce | ||
|
69678f21f5 | ||
|
6f9cae0bcd | ||
|
9b296fc8ed | ||
|
0315dd61d2 | ||
|
bc2100b889 | ||
|
021e76cab7 | ||
|
490d13e333 | ||
|
736c904f65 | ||
|
68a9808942 | ||
|
85e6dc7e5e | ||
|
ace2fded7e | ||
|
0d58fd3f63 | ||
|
ab89bda819 | ||
|
3961e310b1 | ||
|
41ee4c90b3 | ||
|
62612cfdb8 | ||
|
e4af28b8ff | ||
|
099983f7f3 | ||
|
256f082340 | ||
|
dd536eebed | ||
|
5fd38f4595 | ||
|
b0c4eb24e5 | ||
|
8dc5af9406 | ||
|
700170392a | ||
|
20dba6cd86 | ||
|
78a1c1d038 | ||
|
7a65b705d6 | ||
|
055a4cdc10 | ||
|
aef33f708a | ||
|
ac2deb9ae0 | ||
|
26435c1195 | ||
|
9e57fedd80 | ||
|
4036772803 | ||
|
9df1fd9e97 | ||
|
6d3d6cde2d | ||
|
1c2201317f | ||
|
6c8d7baee2 | ||
|
9de913888d | ||
|
b9d4683092 | ||
|
10c61cec02 | ||
|
a6b4ad1d3e | ||
|
3ce1f58a48 | ||
|
4239c2f0e9 | ||
|
077c6563fa | ||
|
29296010db | ||
|
2f33e94881 | ||
|
043448c429 | ||
|
325df61d96 | ||
|
2e1713db49 | ||
|
0edbff13cd | ||
|
fefde77261 | ||
|
dccec469af | ||
|
e5504d2e29 | ||
|
2e2b1e481b | ||
|
46c723c7b6 | ||
|
a060187547 | ||
|
3a55524c86 | ||
|
da0021a9b3 | ||
|
c54c1565ca | ||
|
48cef26ccd | ||
|
d6aa0a814d | ||
|
90b7d71062 | ||
|
017421b232 | ||
|
b0ffad5e76 | ||
|
a197647933 | ||
|
0e27793d25 | ||
|
3262bbea74 | ||
|
42ff99bcc7 | ||
|
e858ed94bc | ||
|
4be5948d97 | ||
|
7249d9136b | ||
|
b81dffcecc | ||
|
dd00c39075 | ||
|
6eda6aea87 | ||
|
c745947281 | ||
|
1826246125 | ||
|
7e5d35d79a | ||
|
9bfbb50ffd | ||
|
61527882d1 | ||
|
92be078783 | ||
|
e80e557f10 | ||
|
e6c77e64a8 | ||
|
12aaa04fea | ||
|
76c653b47e | ||
|
e4d27f8d6b | ||
|
476cd385f8 | ||
|
4d9f466491 | ||
|
3de3443d87 | ||
|
42011ad10c | ||
|
f35bb0a7e8 | ||
|
73b0f48586 | ||
|
69e1d585cc | ||
|
817592e736 | ||
|
67ae6ab3b8 | ||
|
55d7621ef5 | ||
|
4f7ef9c071 | ||
|
cab63a94ac | ||
|
954ec755b8 | ||
|
b9d35ac46d | ||
|
a6ca8de13e | ||
|
346f26177c | ||
|
e41be44397 | ||
|
c3b56164f5 | ||
|
f1fa5e8b91 | ||
|
af691bee1c | ||
|
4f217781cc | ||
|
17fef6c654 | ||
|
a9fb74984b | ||
|
2b3ac412ad | ||
|
38008a5451 | ||
|
3a2928be53 | ||
|
a999033e4b | ||
|
8544c41cc6 | ||
|
6926cda1ee | ||
|
ab8e1ad7e2 | ||
|
921402b0ee | ||
|
24933cc08f | ||
|
09523369b7 | ||
|
ff1d2fa1c3 | ||
|
15559d0be2 | ||
|
45d0de234b | ||
|
faf948b037 | ||
|
0a870ee742 | ||
|
534d5183ed | ||
|
fcdd042d6c | ||
|
39f6b2bbcf | ||
|
244186cf4e | ||
|
7cbbb626c1 | ||
|
b3e31734f5 | ||
|
427f2ff890 | ||
|
02f0449b39 | ||
|
6554447cbc | ||
|
ef9c0c6c6a | ||
|
9658957067 | ||
|
cbcfacb06a | ||
|
06eb64ecf5 | ||
|
24d34eb741 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
rippleapi
|
||||
rippleapi.exe
|
||||
api
|
||||
api.conf
|
||||
debug
|
||||
launch.json
|
||||
|
622
LICENSE
622
LICENSE
@@ -1,3 +1,619 @@
|
||||
Copyright (C) The Ripple Developers - All Rights Reserved
|
||||
Unauthorized copying of this file, via any medium is strictly prohibited
|
||||
Proprietary and confidential
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://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
|
11
README.md
11
README.md
@@ -1,9 +1,6 @@
|
||||
# osuripple/api
|
||||
# rippleapi
|
||||
|
||||
This is the source code for the Ripple API. It's still in its very early development, although I plan on putting it in an early beta phase in about a couple of weeks.
|
||||
This is the source code for Ripple's API.
|
||||
|
||||
[Public roadmap (it's actually just a trello board)](https://trello.com/b/oL43fuPa/api)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
- Origin: https://git.zxq.co/ripple/rippleapi
|
||||
- Mirror: https://github.com/osuripple/api
|
||||
|
@@ -1,27 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.zxq.co/ripple/schiavolib"
|
||||
"github.com/fatih/color"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ErrorHandler is a middleware for gin that takes care of calls to c.Error().
|
||||
func ErrorHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
errs := c.Errors.Errors()
|
||||
if len(errs) != 0 {
|
||||
color.Red("!!! ERRORS OCCURRED !!!")
|
||||
var out string
|
||||
out += fmt.Sprintf("==> %s %s\n", c.Request.Method, c.Request.URL.Path)
|
||||
for _, err := range errs {
|
||||
out += fmt.Sprintf("===> %s\n", err)
|
||||
}
|
||||
color.Red(out)
|
||||
go schiavo.Bunker.Send("Errors occurred:\n```\n" + out + "```")
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +1,11 @@
|
||||
// Package internals has methods that suit none of the API packages.
|
||||
package internals
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
import "github.com/valyala/fasthttp"
|
||||
|
||||
type statusResponse struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
var statusResp = []byte(`{ "status": 1 }`)
|
||||
|
||||
// Status is used for checking the API is up by the ripple website, on the status page.
|
||||
func Status(c *gin.Context) {
|
||||
c.JSON(200, statusResponse{
|
||||
Status: 1,
|
||||
})
|
||||
func Status(c *fasthttp.RequestCtx) {
|
||||
c.Write(statusResp)
|
||||
}
|
||||
|
133
app/method.go
133
app/method.go
@@ -2,83 +2,95 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Method wraps an API method to a HandlerFunc.
|
||||
func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
func Method(f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
initialCaretaker(c, f, privilegesNeeded...)
|
||||
}
|
||||
}
|
||||
|
||||
func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
data, err := ioutil.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
c.Request.Body.Close()
|
||||
func initialCaretaker(c *fasthttp.RequestCtx, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
var doggoTags []string
|
||||
|
||||
token := ""
|
||||
qa := c.Request.URI().QueryArgs()
|
||||
var token string
|
||||
var bearerToken bool
|
||||
switch {
|
||||
case c.Request.Header.Get("X-Ripple-Token") != "":
|
||||
token = c.Request.Header.Get("X-Ripple-Token")
|
||||
case c.Query("token") != "":
|
||||
token = c.Query("token")
|
||||
case c.Query("k") != "":
|
||||
token = c.Query("k")
|
||||
case len(c.Request.Header.Peek("X-Ripple-Token")) > 0:
|
||||
token = string(c.Request.Header.Peek("X-Ripple-Token"))
|
||||
case strings.HasPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer "):
|
||||
token = strings.TrimPrefix(string(c.Request.Header.Peek("Authorization")), "Bearer ")
|
||||
bearerToken = true
|
||||
case len(qa.Peek("token")) > 0:
|
||||
token = string(qa.Peek("token"))
|
||||
case len(qa.Peek("k")) > 0:
|
||||
token = string(qa.Peek("k"))
|
||||
default:
|
||||
token, _ = c.Cookie("X-Ripple-Token")
|
||||
token = string(c.Request.Header.Cookie("rt"))
|
||||
}
|
||||
|
||||
md := common.MethodData{
|
||||
DB: db,
|
||||
RequestData: data,
|
||||
C: c,
|
||||
Ctx: c,
|
||||
Doggo: doggo,
|
||||
R: red,
|
||||
}
|
||||
if token != "" {
|
||||
tokenReal, exists := GetTokenFull(token, db)
|
||||
var (
|
||||
tokenReal common.Token
|
||||
exists bool
|
||||
)
|
||||
if bearerToken {
|
||||
tokenReal, exists = BearerToken(token, db)
|
||||
} else {
|
||||
tokenReal, exists = GetTokenFull(token, db)
|
||||
}
|
||||
if exists {
|
||||
md.User = tokenReal
|
||||
doggoTags = append(doggoTags, "authorised")
|
||||
}
|
||||
}
|
||||
|
||||
// log into datadog that this is an hanayo request
|
||||
if b2s(c.Request.Header.Peek("H-Key")) == cf.HanayoKey && b2s(c.UserAgent()) == "hanayo" {
|
||||
doggoTags = append(doggoTags, "hanayo")
|
||||
}
|
||||
|
||||
doggo.Incr("requests.v1", doggoTags, 1)
|
||||
|
||||
missingPrivileges := 0
|
||||
for _, privilege := range privilegesNeeded {
|
||||
if int(md.User.Privileges)&privilege == 0 {
|
||||
if uint64(md.User.TokenPrivileges)&uint64(privilege) == 0 {
|
||||
missingPrivileges |= privilege
|
||||
}
|
||||
}
|
||||
if missingPrivileges != 0 {
|
||||
c.IndentedJSON(401, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
|
||||
c.SetStatusCode(401)
|
||||
mkjson(c, common.SimpleResponse(401, "You don't have the privilege(s): "+common.Privileges(missingPrivileges).String()+"."))
|
||||
return
|
||||
}
|
||||
|
||||
resp := f(md)
|
||||
if resp.GetCode() == 0 {
|
||||
// Dirty hack to set the code
|
||||
type setCoder interface {
|
||||
SetCode(int)
|
||||
}
|
||||
if newver, can := resp.(setCoder); can {
|
||||
newver.SetCode(500)
|
||||
}
|
||||
if md.HasQuery("pls200") {
|
||||
c.SetStatusCode(200)
|
||||
} else {
|
||||
c.SetStatusCode(resp.GetCode())
|
||||
}
|
||||
|
||||
if _, exists := c.GetQuery("pls200"); exists {
|
||||
c.Writer.WriteHeader(200)
|
||||
if md.HasQuery("callback") {
|
||||
c.Response.Header.Add("Content-Type", "application/javascript; charset=utf-8")
|
||||
} else {
|
||||
c.Writer.WriteHeader(resp.GetCode())
|
||||
}
|
||||
|
||||
if _, exists := c.GetQuery("callback"); exists {
|
||||
c.Header("Content-Type", "application/javascript; charset=utf-8")
|
||||
} else {
|
||||
c.Header("Content-Type", "application/json; charset=utf-8")
|
||||
c.Response.Header.Add("Content-Type", "application/json; charset=utf-8")
|
||||
}
|
||||
|
||||
mkjson(c, resp)
|
||||
@@ -88,22 +100,45 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe
|
||||
var callbackJSONP = regexp.MustCompile(`^[a-zA-Z_\$][a-zA-Z0-9_\$]*$`)
|
||||
|
||||
// mkjson auto indents json, and wraps json into a jsonp callback if specified by the request.
|
||||
// then writes to the gin.Context the data.
|
||||
func mkjson(c *gin.Context, data interface{}) {
|
||||
// then writes to the RequestCtx the data.
|
||||
func mkjson(c *fasthttp.RequestCtx, data interface{}) {
|
||||
exported, err := json.MarshalIndent(data, "", "\t")
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong.", "data": null }`)
|
||||
fmt.Println(err)
|
||||
exported = []byte(`{ "code": 500, "message": "something has gone really really really really really really wrong." }`)
|
||||
}
|
||||
cb := c.Query("callback")
|
||||
cb := string(c.URI().QueryArgs().Peek("callback"))
|
||||
willcb := cb != "" &&
|
||||
len(cb) < 100 &&
|
||||
callbackJSONP.MatchString(cb)
|
||||
if willcb {
|
||||
c.Writer.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
|
||||
c.Write([]byte("/**/ typeof " + cb + " === 'function' && " + cb + "("))
|
||||
}
|
||||
c.Writer.Write(exported)
|
||||
c.Write(exported)
|
||||
if willcb {
|
||||
c.Writer.Write([]byte(");"))
|
||||
c.Write([]byte(");"))
|
||||
}
|
||||
}
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func s2b(s string) []byte {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
}
|
||||
|
@@ -1,38 +1,133 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/thehowl/go-osuapi"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// GetBeatmap retrieves general beatmap information.
|
||||
func GetBeatmap(c *gin.Context, db *sql.DB) {
|
||||
func GetBeatmap(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
var whereClauses []string
|
||||
var params []string
|
||||
var params []interface{}
|
||||
limit := strconv.Itoa(common.InString(1, query(c, "limit"), 500, 500))
|
||||
|
||||
// since value is not stored, silently ignore
|
||||
if c.Query("s") != "" {
|
||||
if query(c, "s") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmapset_id = ?")
|
||||
params = append(params, c.Query("s"))
|
||||
params = append(params, query(c, "s"))
|
||||
}
|
||||
if c.Query("b") != "" {
|
||||
if query(c, "b") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmap_id = ?")
|
||||
params = append(params, c.Query("b"))
|
||||
params = append(params, query(c, "b"))
|
||||
// b is unique, so change limit to 1
|
||||
limit = "1"
|
||||
}
|
||||
if c.Query("u") != "" {
|
||||
wc, p := genUser(c, db)
|
||||
whereClauses = append(whereClauses, wc)
|
||||
params = append(params, p)
|
||||
// creator is not stored, silently ignore u and type
|
||||
if query(c, "m") != "" {
|
||||
m := genmode(query(c, "m"))
|
||||
if m == "std" {
|
||||
// Since STD beatmaps are converted, all of the diffs must be != 0
|
||||
for _, i := range modes {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_"+i+" != 0")
|
||||
}
|
||||
// silently ignore m
|
||||
// silently ignore a
|
||||
if c.Query("h") != "" {
|
||||
} else {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_"+m+" != 0")
|
||||
if query(c, "a") == "1" {
|
||||
whereClauses = append(whereClauses, "beatmaps.difficulty_std = 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
if query(c, "h") != "" {
|
||||
whereClauses = append(whereClauses, "beatmaps.beatmap_md5 = ?")
|
||||
params = append(params, c.Query("h"))
|
||||
params = append(params, query(c, "h"))
|
||||
}
|
||||
|
||||
//bm := osuapi.Beatmap{}
|
||||
where := strings.Join(whereClauses, " AND ")
|
||||
if where != "" {
|
||||
where = "WHERE " + where
|
||||
}
|
||||
|
||||
//db.Query("SELECT beatmaps.beatmapset_id, beatmaps.beatmap FROM ")
|
||||
rows, err := db.Query(`SELECT
|
||||
beatmapset_id, beatmap_id, ranked, hit_length,
|
||||
song_name, beatmap_md5, ar, od, bpm, playcount,
|
||||
passcount, max_combo, difficulty_std, difficulty_taiko, difficulty_ctb, difficulty_mania,
|
||||
latest_update
|
||||
|
||||
FROM beatmaps `+where+" ORDER BY id DESC LIMIT "+limit,
|
||||
params...)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
|
||||
var bms []osuapi.Beatmap
|
||||
for rows.Next() {
|
||||
var (
|
||||
bm osuapi.Beatmap
|
||||
rawRankedStatus int
|
||||
rawName string
|
||||
rawLastUpdate common.UnixTimestamp
|
||||
diffs [4]float64
|
||||
)
|
||||
err := rows.Scan(
|
||||
&bm.BeatmapSetID, &bm.BeatmapID, &rawRankedStatus, &bm.HitLength,
|
||||
&rawName, &bm.FileMD5, &bm.ApproachRate, &bm.OverallDifficulty, &bm.BPM, &bm.Playcount,
|
||||
&bm.Passcount, &bm.MaxCombo, &diffs[0], &diffs[1], &diffs[2], &diffs[3],
|
||||
&rawLastUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
continue
|
||||
}
|
||||
bm.TotalLength = bm.HitLength
|
||||
bm.LastUpdate = osuapi.MySQLDate(rawLastUpdate)
|
||||
if rawRankedStatus >= 2 {
|
||||
bm.ApprovedDate = osuapi.MySQLDate(rawLastUpdate)
|
||||
}
|
||||
// zero value of ApprovedStatus == osuapi.StatusPending, so /shrug
|
||||
bm.Approved = rippleToOsuRankedStatus[rawRankedStatus]
|
||||
bm.Artist, bm.Title, bm.DiffName = parseDiffName(rawName)
|
||||
for i, diffVal := range diffs {
|
||||
if diffVal != 0 {
|
||||
bm.Mode = osuapi.Mode(i)
|
||||
bm.DifficultyRating = diffVal
|
||||
break
|
||||
}
|
||||
}
|
||||
bms = append(bms, bm)
|
||||
}
|
||||
|
||||
json(c, 200, bms)
|
||||
}
|
||||
|
||||
var rippleToOsuRankedStatus = map[int]osuapi.ApprovedStatus{
|
||||
0: osuapi.StatusPending,
|
||||
1: osuapi.StatusWIP, // it means "needs updating", as the one in the db needs to be updated, but whatever
|
||||
2: osuapi.StatusRanked,
|
||||
3: osuapi.StatusApproved,
|
||||
4: osuapi.StatusQualified,
|
||||
5: osuapi.StatusLoved,
|
||||
}
|
||||
|
||||
// buggy diffname parser
|
||||
func parseDiffName(name string) (author string, title string, diffName string) {
|
||||
parts := strings.SplitN(name, " - ", 2)
|
||||
author = parts[0]
|
||||
if len(parts) > 1 {
|
||||
title = parts[1]
|
||||
if s := strings.Index(title, " ["); s != -1 {
|
||||
diffName = title[s+2:]
|
||||
if len(diffName) != 0 && diffName[len(diffName)-1] == ']' {
|
||||
diffName = diffName[:len(diffName)-1]
|
||||
}
|
||||
title = title[:s]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@@ -2,27 +2,21 @@ package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_json "encoding/json"
|
||||
"strconv"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var modes = []string{"std", "taiko", "ctb", "mania"}
|
||||
|
||||
var defaultResponse = []struct{}{}
|
||||
|
||||
func genmode(m string) string {
|
||||
switch m {
|
||||
case "1":
|
||||
m = "taiko"
|
||||
case "2":
|
||||
m = "ctb"
|
||||
case "3":
|
||||
m = "mania"
|
||||
default:
|
||||
m = "std"
|
||||
}
|
||||
return m
|
||||
i := genmodei(m)
|
||||
return modes[i]
|
||||
}
|
||||
func genmodei(m string) int {
|
||||
v := common.Int(m)
|
||||
@@ -31,34 +25,51 @@ func genmodei(m string) int {
|
||||
}
|
||||
return v
|
||||
}
|
||||
func rankable(m string) bool {
|
||||
x := genmodei(m)
|
||||
return x == 0 || x == 3
|
||||
}
|
||||
|
||||
func genUser(c *gin.Context, db *sql.DB) (string, string) {
|
||||
func genUser(c *fasthttp.RequestCtx, db *sqlx.DB) (string, string) {
|
||||
var whereClause string
|
||||
var p string
|
||||
|
||||
// used in second case of switch
|
||||
s, err := strconv.Atoi(c.Query("u"))
|
||||
s, err := strconv.Atoi(query(c, "u"))
|
||||
|
||||
switch {
|
||||
// We know for sure that it's an username.
|
||||
case c.Query("type") == "string":
|
||||
whereClause = "users.username = ?"
|
||||
p = c.Query("u")
|
||||
case query(c, "type") == "string":
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
// It could be an user ID, so we look for an user with that username first.
|
||||
case err == nil:
|
||||
err = db.QueryRow("SELECT id FROM users WHERE id = ? LIMIT 1", s).Scan(&p)
|
||||
if err == sql.ErrNoRows {
|
||||
// If no user with that userID were found, assume username.
|
||||
p = c.Query("u")
|
||||
whereClause = "users.username = ?"
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
} else {
|
||||
// An user with that userID was found. Thus it's an userID.
|
||||
whereClause = "users.id = ?"
|
||||
}
|
||||
// u contains letters, so it's an username.
|
||||
default:
|
||||
p = c.Query("u")
|
||||
whereClause = "users.username = ?"
|
||||
whereClause = "users.username_safe = ?"
|
||||
p = common.SafeUsername(query(c, "u"))
|
||||
}
|
||||
return whereClause, p
|
||||
}
|
||||
|
||||
func query(c *fasthttp.RequestCtx, s string) string {
|
||||
return string(c.QueryArgs().Peek(s))
|
||||
}
|
||||
|
||||
func json(c *fasthttp.RequestCtx, code int, data interface{}) {
|
||||
c.SetStatusCode(code)
|
||||
d, err := _json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Write(d)
|
||||
}
|
||||
|
@@ -2,12 +2,11 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// GetMatch retrieves general match information.
|
||||
func GetMatch(c *gin.Context, db *sql.DB) {
|
||||
c.JSON(200, defaultResponse)
|
||||
func GetMatch(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
json(c, 200, defaultResponse)
|
||||
}
|
||||
|
98
app/peppy/score.go
Normal file
98
app/peppy/score.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// GetScores retrieve information about the top 100 scores of a specified beatmap.
|
||||
func GetScores(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
if query(c, "b") == "" {
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var beatmapMD5 string
|
||||
err := db.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", query(c, "b"))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
case err != nil:
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var sb = "scores.score"
|
||||
if rankable(query(c, "m")) {
|
||||
sb = "scores.pp"
|
||||
}
|
||||
var (
|
||||
extraWhere string
|
||||
extraParams []interface{}
|
||||
)
|
||||
if query(c, "u") != "" {
|
||||
w, p := genUser(c, db)
|
||||
extraWhere = "AND " + w
|
||||
extraParams = append(extraParams, p)
|
||||
}
|
||||
mods := common.Int(query(c, "mods"))
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
scores.id, scores.score, users.username, scores.300_count, scores.100_count,
|
||||
scores.50_count, scores.misses_count, scores.gekis_count, scores.katus_count,
|
||||
scores.max_combo, scores.full_combo, scores.mods, users.id, scores.time, scores.pp,
|
||||
scores.accuracy
|
||||
FROM scores
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
WHERE scores.completed = '3'
|
||||
AND users.privileges & 1 > 0
|
||||
AND scores.beatmap_md5 = ?
|
||||
AND scores.play_mode = ?
|
||||
AND scores.mods & ? = ?
|
||||
`+extraWhere+`
|
||||
ORDER BY `+sb+` DESC LIMIT `+strconv.Itoa(common.InString(1, query(c, "limit"), 100, 50)),
|
||||
append([]interface{}{beatmapMD5, genmodei(query(c, "m")), mods, mods}, extraParams...)...)
|
||||
if err != nil {
|
||||
common.Err(c, err)
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var results []osuapi.GSScore
|
||||
for rows.Next() {
|
||||
var (
|
||||
s osuapi.GSScore
|
||||
fullcombo bool
|
||||
mods int
|
||||
date common.UnixTimestamp
|
||||
accuracy float64
|
||||
)
|
||||
err := rows.Scan(
|
||||
&s.ScoreID, &s.Score.Score, &s.Username, &s.Count300, &s.Count100,
|
||||
&s.Count50, &s.CountMiss, &s.CountGeki, &s.CountKatu,
|
||||
&s.MaxCombo, &fullcombo, &mods, &s.UserID, &date, &s.PP,
|
||||
&accuracy,
|
||||
)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
common.Err(c, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.FullCombo = osuapi.OsuBool(fullcombo)
|
||||
s.Mods = osuapi.Mods(mods)
|
||||
s.Date = osuapi.MySQLDate(date)
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(osuapi.Mode(genmodei(query(c, "m"))), s.Mods,
|
||||
accuracy, s.Count300, s.Count100, s.Count50, s.CountMiss))
|
||||
results = append(results, s)
|
||||
}
|
||||
json(c, 200, results)
|
||||
return
|
||||
}
|
@@ -4,54 +4,61 @@ package peppy
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.zxq.co/ripple/ocl"
|
||||
"github.com/gin-gonic/gin"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/thehowl/go-osuapi"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/redis.v5"
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// R is a redis client.
|
||||
var R *redis.Client
|
||||
|
||||
// GetUser retrieves general user information.
|
||||
func GetUser(c *gin.Context, db *sql.DB) {
|
||||
if c.Query("u") == "" {
|
||||
c.JSON(200, defaultResponse)
|
||||
func GetUser(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
if query(c, "u") == "" {
|
||||
json(c, 200, defaultResponse)
|
||||
return
|
||||
}
|
||||
var user osuapi.User
|
||||
whereClause, p := genUser(c, db)
|
||||
whereClause = "WHERE " + whereClause
|
||||
|
||||
mode := genmode(c.Query("m"))
|
||||
mode := genmode(query(c, "m"))
|
||||
|
||||
var display bool
|
||||
err := db.QueryRow(fmt.Sprintf(
|
||||
`SELECT
|
||||
users.id, users.username,
|
||||
users_stats.playcount_%s, users_stats.ranked_score_%s, users_stats.total_score_%s,
|
||||
leaderboard_%s.position, users_stats.pp_%s, users_stats.avg_accuracy_%s,
|
||||
users_stats.country, users_stats.show_country
|
||||
users_stats.pp_%s, users_stats.avg_accuracy_%s,
|
||||
users_stats.country
|
||||
FROM users
|
||||
LEFT JOIN users_stats ON users_stats.id = users.id
|
||||
LEFT JOIN leaderboard_%s ON leaderboard_%s.user = users.id
|
||||
%s
|
||||
LIMIT 1`,
|
||||
mode, mode, mode, mode, mode, mode, mode, mode, whereClause,
|
||||
mode, mode, mode, mode, mode, whereClause,
|
||||
), p).Scan(
|
||||
&user.UserID, &user.Username,
|
||||
&user.Playcount, &user.RankedScore, &user.TotalScore,
|
||||
&user.Rank, &user.PP, &user.Accuracy,
|
||||
&user.Country, &display,
|
||||
&user.PP, &user.Accuracy,
|
||||
&user.Country,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(200, defaultResponse)
|
||||
json(c, 200, defaultResponse)
|
||||
if err != sql.ErrNoRows {
|
||||
c.Error(err)
|
||||
common.Err(c, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !display {
|
||||
user.Country = "XX"
|
||||
}
|
||||
|
||||
user.Rank = int(R.ZRevRank("ripple:leaderboard:"+mode, strconv.Itoa(user.UserID)).Val()) + 1
|
||||
user.CountryRank = int(R.ZRevRank("ripple:leaderboard:"+mode+":"+strings.ToLower(user.Country), strconv.Itoa(user.UserID)).Val()) + 1
|
||||
user.Level = ocl.GetLevelPrecise(user.TotalScore)
|
||||
|
||||
c.JSON(200, []osuapi.User{user})
|
||||
json(c, 200, []osuapi.User{user})
|
||||
}
|
||||
|
@@ -1,36 +1,35 @@
|
||||
package peppy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"git.zxq.co/x/getrank"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// GetUserRecent retrieves an user's recent scores.
|
||||
func GetUserRecent(c *gin.Context, db *sql.DB) {
|
||||
getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, c.Query("limit"), 50, 10))
|
||||
func GetUserRecent(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
getUserX(c, db, "ORDER BY scores.time DESC", common.InString(1, query(c, "limit"), 50, 10))
|
||||
}
|
||||
|
||||
// GetUserBest retrieves an user's best scores.
|
||||
func GetUserBest(c *gin.Context, db *sql.DB) {
|
||||
func GetUserBest(c *fasthttp.RequestCtx, db *sqlx.DB) {
|
||||
var sb string
|
||||
if genmodei(c.Query("m")) == 0 {
|
||||
if rankable(query(c, "m")) {
|
||||
sb = "scores.pp"
|
||||
} else {
|
||||
sb = "scores.score"
|
||||
}
|
||||
getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, c.Query("limit"), 100, 10))
|
||||
getUserX(c, db, "AND completed = '3' ORDER BY "+sb+" DESC", common.InString(1, query(c, "limit"), 100, 10))
|
||||
}
|
||||
|
||||
func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
|
||||
func getUserX(c *fasthttp.RequestCtx, db *sqlx.DB, orderBy string, limit int) {
|
||||
whereClause, p := genUser(c, db)
|
||||
query := fmt.Sprintf(
|
||||
sqlQuery := fmt.Sprintf(
|
||||
`SELECT
|
||||
beatmaps.beatmap_id, scores.score, scores.max_combo,
|
||||
scores.300_count, scores.100_count, scores.50_count,
|
||||
@@ -40,22 +39,22 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
|
||||
FROM scores
|
||||
LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
LEFT JOIN users ON scores.userid = users.id
|
||||
WHERE %s AND scores.play_mode = ? AND users.allowed = '1'
|
||||
WHERE %s AND scores.play_mode = ? AND users.privileges & 1 > 0
|
||||
%s
|
||||
LIMIT %d`, whereClause, orderBy, limit,
|
||||
)
|
||||
scores := make([]osuapi.GUSScore, 0, limit)
|
||||
m := genmodei(c.Query("m"))
|
||||
rows, err := db.Query(query, p, m)
|
||||
m := genmodei(query(c, "m"))
|
||||
rows, err := db.Query(sqlQuery, p, m)
|
||||
if err != nil {
|
||||
c.JSON(200, defaultResponse)
|
||||
c.Error(err)
|
||||
json(c, 200, defaultResponse)
|
||||
common.Err(c, err)
|
||||
return
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
curscore osuapi.GUSScore
|
||||
rawTime string
|
||||
rawTime common.UnixTimestamp
|
||||
acc float64
|
||||
fc bool
|
||||
mods int
|
||||
@@ -69,8 +68,8 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
|
||||
&curscore.PP, &acc,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(200, defaultResponse)
|
||||
c.Error(err)
|
||||
json(c, 200, defaultResponse)
|
||||
common.Err(c, err)
|
||||
return
|
||||
}
|
||||
if bid == nil {
|
||||
@@ -80,13 +79,7 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
|
||||
}
|
||||
curscore.FullCombo = osuapi.OsuBool(fc)
|
||||
curscore.Mods = osuapi.Mods(mods)
|
||||
t, err := time.Parse(common.OsuTimeFormat, rawTime)
|
||||
if err != nil {
|
||||
c.JSON(200, defaultResponse)
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
curscore.Date = osuapi.MySQLDate(t)
|
||||
curscore.Date = osuapi.MySQLDate(rawTime)
|
||||
curscore.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(m),
|
||||
curscore.Mods,
|
||||
@@ -98,5 +91,5 @@ func getUserX(c *gin.Context, db *sql.DB, orderBy string, limit int) {
|
||||
))
|
||||
scores = append(scores, curscore)
|
||||
}
|
||||
c.JSON(200, scores)
|
||||
json(c, 200, scores)
|
||||
}
|
||||
|
@@ -1,14 +1,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// PeppyMethod generates a method for the peppyapi
|
||||
func PeppyMethod(a func(c *gin.Context, db *sql.DB)) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
func PeppyMethod(a func(c *fasthttp.RequestCtx, db *sqlx.DB)) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
doggo.Incr("requests.peppy", nil, 1)
|
||||
|
||||
// I have no idea how, but I manged to accidentally string the first 4
|
||||
// letters of the alphabet into a single function call.
|
||||
a(c, db)
|
||||
|
106
app/router.go
Normal file
106
app/router.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/buaazp/fasthttprouter"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
r *fasthttprouter.Router
|
||||
}
|
||||
|
||||
func (r router) Method(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
r.r.GET(path, wrap(Method(f, privilegesNeeded...)))
|
||||
}
|
||||
func (r router) POSTMethod(path string, f func(md common.MethodData) common.CodeMessager, privilegesNeeded ...int) {
|
||||
r.r.POST(path, wrap(Method(f, privilegesNeeded...)))
|
||||
}
|
||||
func (r router) Peppy(path string, a func(c *fasthttp.RequestCtx, db *sqlx.DB)) {
|
||||
r.r.GET(path, wrap(PeppyMethod(a)))
|
||||
}
|
||||
func (r router) GET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, wrap(handle))
|
||||
}
|
||||
func (r router) PlainGET(path string, handle fasthttp.RequestHandler) {
|
||||
r.r.GET(path, handle)
|
||||
}
|
||||
|
||||
const (
|
||||
// \x1b is escape code for ESC
|
||||
// <ESC>[<n>m is escape sequence for a certain colour
|
||||
// no IP is written out because of the hundreds of possible ways to pass IPs
|
||||
// to a request when using a reverse proxy
|
||||
// this is partly inspired from gin, though made even more simplistic.
|
||||
fmtString = "%s | %15s |\x1b[%sm %3d \x1b[0m %-7s %s\n"
|
||||
// a kind of human readable RFC3339
|
||||
timeFormat = "2006-01-02 15:04:05"
|
||||
// color reference
|
||||
// http://misc.flogisoft.com/bash/tip_colors_and_formatting
|
||||
colorOk = "42" // green
|
||||
colorError = "41" // red
|
||||
)
|
||||
|
||||
// wrap returns a function that wraps around handle, providing middleware
|
||||
// functionality to apply to all API calls, which is to say:
|
||||
// - logging
|
||||
// - panic recovery (reporting to sentry)
|
||||
// - gzipping
|
||||
func wrap(handle fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||||
return func(c *fasthttp.RequestCtx) {
|
||||
start := time.Now()
|
||||
|
||||
doggo.Incr("requests", nil, 1)
|
||||
|
||||
defer func() {
|
||||
if rval := recover(); rval != nil {
|
||||
var err error
|
||||
switch rval := rval.(type) {
|
||||
case string:
|
||||
err = errors.New(rval)
|
||||
case error:
|
||||
err = rval
|
||||
default:
|
||||
err = fmt.Errorf("%v - %#v", rval, rval)
|
||||
}
|
||||
common.Err(c, err)
|
||||
c.SetStatusCode(500)
|
||||
c.SetBodyString(`{ "code": 500, "message": "something really bad happened" }`)
|
||||
}
|
||||
|
||||
// switch color to colorError if statusCode is in [500;600)
|
||||
color := colorOk
|
||||
statusCode := c.Response.StatusCode()
|
||||
if statusCode >= 500 && statusCode < 600 {
|
||||
color = colorError
|
||||
}
|
||||
|
||||
if bytes.Contains(c.Request.Header.Peek("Accept-Encoding"), s2b("gzip")) {
|
||||
c.Response.Header.Add("Content-Encoding", "gzip")
|
||||
c.Response.Header.Add("Vary", "Accept-Encoding")
|
||||
b := c.Response.Body()
|
||||
c.Response.ResetBody()
|
||||
fasthttp.WriteGzip(c.Response.BodyWriter(), b)
|
||||
}
|
||||
|
||||
// print stuff
|
||||
fmt.Printf(
|
||||
fmtString,
|
||||
time.Now().Format(timeFormat),
|
||||
time.Since(start).String(),
|
||||
color,
|
||||
statusCode,
|
||||
c.Method(),
|
||||
c.Path(),
|
||||
)
|
||||
}()
|
||||
|
||||
handle(c)
|
||||
}
|
||||
}
|
193
app/start.go
193
app/start.go
@@ -1,97 +1,164 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/app/internals"
|
||||
"git.zxq.co/ripple/rippleapi/app/peppy"
|
||||
"git.zxq.co/ripple/rippleapi/app/v1"
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/gin-gonic/contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/DataDog/datadog-go/statsd"
|
||||
fhr "github.com/buaazp/fasthttprouter"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/redis.v5"
|
||||
"zxq.co/ripple/rippleapi/app/internals"
|
||||
"zxq.co/ripple/rippleapi/app/peppy"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/ripple/rippleapi/app/websockets"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
var (
|
||||
db *sqlx.DB
|
||||
cf common.Conf
|
||||
doggo *statsd.Client
|
||||
red *redis.Client
|
||||
)
|
||||
|
||||
// Start begins taking HTTP connections.
|
||||
func Start(conf common.Conf, dbO *sql.DB) *gin.Engine {
|
||||
func Start(conf common.Conf, dbO *sqlx.DB) *fhr.Router {
|
||||
db = dbO
|
||||
r := gin.Default()
|
||||
r.Use(gzip.Gzip(gzip.DefaultCompression), ErrorHandler())
|
||||
cf = conf
|
||||
|
||||
api := r.Group("/api")
|
||||
rawRouter := fhr.New()
|
||||
r := router{rawRouter}
|
||||
// TODO: add back gzip
|
||||
// TODO: add logging
|
||||
// TODO: add sentry panic recovering
|
||||
|
||||
// sentry
|
||||
if conf.SentryDSN != "" {
|
||||
ravenClient, err := raven.New(conf.SentryDSN)
|
||||
ravenClient.SetRelease(common.Version)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else {
|
||||
// r.Use(Recovery(ravenClient, false))
|
||||
common.RavenClient = ravenClient
|
||||
}
|
||||
}
|
||||
|
||||
// datadog
|
||||
var err error
|
||||
doggo, err = statsd.New("127.0.0.1:8125")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
doggo.Namespace = "api."
|
||||
|
||||
// redis
|
||||
red = redis.NewClient(&redis.Options{
|
||||
Addr: conf.RedisAddr,
|
||||
Password: conf.RedisPassword,
|
||||
DB: conf.RedisDB,
|
||||
})
|
||||
peppy.R = red
|
||||
|
||||
// token updater
|
||||
go tokenUpdater(db)
|
||||
|
||||
// start websocket
|
||||
websockets.Start(red, db)
|
||||
|
||||
// peppyapi
|
||||
{
|
||||
gv1 := api.Group("/v1")
|
||||
r.Peppy("/api/get_user", peppy.GetUser)
|
||||
r.Peppy("/api/get_match", peppy.GetMatch)
|
||||
r.Peppy("/api/get_user_recent", peppy.GetUserRecent)
|
||||
r.Peppy("/api/get_user_best", peppy.GetUserBest)
|
||||
r.Peppy("/api/get_scores", peppy.GetScores)
|
||||
r.Peppy("/api/get_beatmaps", peppy.GetBeatmap)
|
||||
}
|
||||
|
||||
// v1 API
|
||||
{
|
||||
gv1.POST("/tokens/new", Method(v1.TokenNewPOST))
|
||||
gv1.GET("/tokens/self/delete", Method(v1.TokenSelfDeleteGET))
|
||||
// These require an user to pass the password in cleartext and are
|
||||
// as such really insecure.
|
||||
//r.POSTMethod("/api/v1/tokens", v1.TokenNewPOST)
|
||||
//r.POSTMethod("/api/v1/tokens/new", v1.TokenNewPOST)
|
||||
r.POSTMethod("/api/v1/tokens/self/delete", v1.TokenSelfDeletePOST)
|
||||
|
||||
// Auth-free API endpoints
|
||||
gv1.GET("/ping", Method(v1.PingGET))
|
||||
gv1.GET("/surprise_me", Method(v1.SurpriseMeGET))
|
||||
gv1.GET("/privileges", Method(v1.PrivilegesGET))
|
||||
gv1.GET("/doc", Method(v1.DocGET))
|
||||
gv1.GET("/doc/content", Method(v1.DocContentGET))
|
||||
gv1.GET("/doc/rules", Method(v1.DocRulesGET))
|
||||
|
||||
// Read privilege required
|
||||
gv1.GET("/users", Method(v1.UsersGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/self", Method(v1.UserSelfGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/whatid", Method(v1.UserWhatsTheIDGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/full", Method(v1.UserFullGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/userpage", Method(v1.UserUserpageGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/lookup", Method(v1.UserLookupGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/scores/best", Method(v1.UserScoresBestGET, common.PrivilegeRead))
|
||||
gv1.GET("/users/scores/recent", Method(v1.UserScoresRecentGET, common.PrivilegeRead))
|
||||
gv1.GET("/badges", Method(v1.BadgesGET, common.PrivilegeRead))
|
||||
gv1.GET("/beatmaps", Method(v1.BeatmapGET, common.PrivilegeRead))
|
||||
gv1.GET("/leaderboard", Method(v1.LeaderboardGET, common.PrivilegeRead))
|
||||
gv1.GET("/tokens", Method(v1.TokenGET, common.PrivilegeRead))
|
||||
gv1.GET("/tokens/self", Method(v1.TokenSelfGET, common.PrivilegeRead))
|
||||
// Auth-free API endpoints (public data)
|
||||
r.Method("/api/v1/ping", v1.PingGET)
|
||||
r.Method("/api/v1/surprise_me", v1.SurpriseMeGET)
|
||||
r.Method("/api/v1/users", v1.UsersGET)
|
||||
r.Method("/api/v1/users/whatid", v1.UserWhatsTheIDGET)
|
||||
r.Method("/api/v1/users/full", v1.UserFullGET)
|
||||
r.Method("/api/v1/users/userpage", v1.UserUserpageGET)
|
||||
r.Method("/api/v1/users/lookup", v1.UserLookupGET)
|
||||
r.Method("/api/v1/users/scores/best", v1.UserScoresBestGET)
|
||||
r.Method("/api/v1/users/scores/recent", v1.UserScoresRecentGET)
|
||||
r.Method("/api/v1/badges", v1.BadgesGET)
|
||||
r.Method("/api/v1/badges/members", v1.BadgeMembersGET)
|
||||
r.Method("/api/v1/beatmaps", v1.BeatmapGET)
|
||||
r.Method("/api/v1/leaderboard", v1.LeaderboardGET)
|
||||
r.Method("/api/v1/tokens", v1.TokenGET)
|
||||
r.Method("/api/v1/users/self", v1.UserSelfGET)
|
||||
r.Method("/api/v1/tokens/self", v1.TokenSelfGET)
|
||||
r.Method("/api/v1/blog/posts", v1.BlogPostsGET)
|
||||
r.Method("/api/v1/scores", v1.ScoresGET)
|
||||
r.Method("/api/v1/beatmaps/rank_requests/status", v1.BeatmapRankRequestsStatusGET)
|
||||
|
||||
// ReadConfidential privilege required
|
||||
gv1.GET("/friends", Method(v1.FriendsGET, common.PrivilegeReadConfidential))
|
||||
gv1.GET("/friends/with", Method(v1.FriendsWithGET, common.PrivilegeReadConfidential))
|
||||
r.Method("/api/v1/friends", v1.FriendsGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/friends/with", v1.FriendsWithGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/donor_info", v1.UsersSelfDonorInfoGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/favourite_mode", v1.UsersSelfFavouriteModeGET, common.PrivilegeReadConfidential)
|
||||
r.Method("/api/v1/users/self/settings", v1.UsersSelfSettingsGET, common.PrivilegeReadConfidential)
|
||||
|
||||
// Write privilege required
|
||||
gv1.GET("/friends/add", Method(v1.FriendsAddGET, common.PrivilegeWrite))
|
||||
gv1.GET("/friends/del", Method(v1.FriendsDelGET, common.PrivilegeWrite))
|
||||
r.POSTMethod("/api/v1/friends/add", v1.FriendsAddPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/friends/del", v1.FriendsDelPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/users/self/settings", v1.UsersSelfSettingsPOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/users/self/userpage", v1.UserSelfUserpagePOST, common.PrivilegeWrite)
|
||||
r.POSTMethod("/api/v1/beatmaps/rank_requests", v1.BeatmapRankRequestsSubmitPOST, common.PrivilegeWrite)
|
||||
|
||||
// Admin: beatmap
|
||||
gv1.POST("/beatmaps/set_status", Method(v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap))
|
||||
gv1.GET("/beatmaps/ranked_frozen_full", Method(v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap))
|
||||
r.POSTMethod("/api/v1/beatmaps/set_status", v1.BeatmapSetStatusPOST, common.PrivilegeBeatmap)
|
||||
r.Method("/api/v1/beatmaps/ranked_frozen_full", v1.BeatmapRankedFrozenFullGET, common.PrivilegeBeatmap)
|
||||
|
||||
// Admin: user managing
|
||||
gv1.POST("/users/manage/set_allowed", Method(v1.UserManageSetAllowedPOST, common.PrivilegeManageUser))
|
||||
r.POSTMethod("/api/v1/users/manage/set_allowed", v1.UserManageSetAllowedPOST, common.PrivilegeManageUser)
|
||||
|
||||
// M E T A
|
||||
// E T "wow thats so meta"
|
||||
// T E -- the one who said "wow thats so meta"
|
||||
// A T E M
|
||||
gv1.GET("/meta/restart", Method(v1.MetaRestartGET, common.PrivilegeAPIMeta))
|
||||
gv1.GET("/meta/kill", Method(v1.MetaKillGET, common.PrivilegeAPIMeta))
|
||||
gv1.GET("/meta/up_since", Method(v1.MetaUpSinceGET, common.PrivilegeAPIMeta))
|
||||
gv1.GET("/meta/update", Method(v1.MetaUpdateGET, common.PrivilegeAPIMeta))
|
||||
r.Method("/api/v1/meta/restart", v1.MetaRestartGET, common.PrivilegeAPIMeta)
|
||||
r.Method("/api/v1/meta/kill", v1.MetaKillGET, common.PrivilegeAPIMeta)
|
||||
r.Method("/api/v1/meta/up_since", v1.MetaUpSinceGET, common.PrivilegeAPIMeta)
|
||||
r.Method("/api/v1/meta/update", v1.MetaUpdateGET, common.PrivilegeAPIMeta)
|
||||
|
||||
// User Managing + meta
|
||||
gv1.GET("/tokens/fix_privileges", Method(v1.TokenFixPrivilegesGET,
|
||||
common.PrivilegeManageUser, common.PrivilegeAPIMeta))
|
||||
r.POSTMethod("/api/v1/tokens/fix_privileges", v1.TokenFixPrivilegesPOST,
|
||||
common.PrivilegeManageUser, common.PrivilegeAPIMeta)
|
||||
}
|
||||
|
||||
api.GET("/status", internals.Status)
|
||||
|
||||
// peppyapi
|
||||
api.GET("/get_user", PeppyMethod(peppy.GetUser))
|
||||
api.GET("/get_match", PeppyMethod(peppy.GetMatch))
|
||||
api.GET("/get_user_recent", PeppyMethod(peppy.GetUserRecent))
|
||||
api.GET("/get_user_best", PeppyMethod(peppy.GetUserBest))
|
||||
// Websocket API
|
||||
{
|
||||
r.PlainGET("/api/v1/ws", websockets.WebsocketV1Entry)
|
||||
}
|
||||
|
||||
r.NoRoute(v1.Handle404)
|
||||
|
||||
return r
|
||||
/*if conf.Unix {
|
||||
panic(r.RunUnix(conf.ListenTo))
|
||||
// in the new osu-web, the old endpoints are also in /v1 it seems. So /shrug
|
||||
{
|
||||
r.Peppy("/api/v1/get_user", peppy.GetUser)
|
||||
r.Peppy("/api/v1/get_match", peppy.GetMatch)
|
||||
r.Peppy("/api/v1/get_user_recent", peppy.GetUserRecent)
|
||||
r.Peppy("/api/v1/get_user_best", peppy.GetUserBest)
|
||||
r.Peppy("/api/v1/get_scores", peppy.GetScores)
|
||||
r.Peppy("/api/v1/get_beatmaps", peppy.GetBeatmap)
|
||||
}
|
||||
panic(r.Run(conf.ListenTo))*/
|
||||
|
||||
r.GET("/api/status", internals.Status)
|
||||
|
||||
rawRouter.NotFound = v1.Handle404
|
||||
|
||||
return rawRouter
|
||||
}
|
||||
|
110
app/tokens.go
110
app/tokens.go
@@ -2,26 +2,40 @@ package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// GetTokenFull retrieves an user ID and their token privileges knowing their API token.
|
||||
func GetTokenFull(token string, db *sql.DB) (common.Token, bool) {
|
||||
var t common.Token
|
||||
var privs uint64
|
||||
var priv8 bool
|
||||
err := db.QueryRow("SELECT id, user, privileges, private FROM tokens WHERE token = ? LIMIT 1",
|
||||
func GetTokenFull(token string, db *sqlx.DB) (common.Token, bool) {
|
||||
var (
|
||||
t common.Token
|
||||
tokenPrivsRaw uint64
|
||||
userPrivsRaw uint64
|
||||
priv8 bool
|
||||
)
|
||||
err := db.QueryRow(`SELECT
|
||||
t.id, t.user, t.privileges, t.private, u.privileges
|
||||
FROM tokens t
|
||||
LEFT JOIN users u ON u.id = t.user
|
||||
WHERE token = ? LIMIT 1`,
|
||||
fmt.Sprintf("%x", md5.Sum([]byte(token)))).
|
||||
Scan(
|
||||
&t.ID, &t.UserID, &privs, &priv8,
|
||||
&t.ID, &t.UserID, &tokenPrivsRaw, &priv8, &userPrivsRaw,
|
||||
)
|
||||
t.Privileges = common.Privileges(privs)
|
||||
updateTokens <- t.ID
|
||||
if priv8 {
|
||||
privs = common.PrivilegeRead | common.PrivilegeReadConfidential | common.PrivilegeWrite
|
||||
// all privileges, they'll get removed by canOnly anyway.
|
||||
tokenPrivsRaw = (common.PrivilegeBeatmap << 1) - 1
|
||||
}
|
||||
t.UserPrivileges = common.UserPrivileges(userPrivsRaw)
|
||||
t.TokenPrivileges = common.Privileges(tokenPrivsRaw).CanOnly(t.UserPrivileges)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.Token{}, false
|
||||
@@ -32,3 +46,81 @@ func GetTokenFull(token string, db *sql.DB) (common.Token, bool) {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
|
||||
var updateTokens = make(chan int, 100)
|
||||
|
||||
func tokenUpdater(db *sqlx.DB) {
|
||||
for {
|
||||
// prepare a queue of tokens to update.
|
||||
tokensToUpdate := make([]int, 0, 50)
|
||||
AwaitLoop:
|
||||
for {
|
||||
// if we got ten, move on and update
|
||||
if len(tokensToUpdate) >= 50 {
|
||||
break
|
||||
}
|
||||
// if we ain't got any, add what we get straight from updateTokens
|
||||
if len(tokensToUpdate) == 0 {
|
||||
x := <-updateTokens
|
||||
tokensToUpdate = append(tokensToUpdate, x)
|
||||
continue
|
||||
}
|
||||
|
||||
// otherwise, wait from updateTokens with a timeout of 10 seconds
|
||||
select {
|
||||
case x := <-updateTokens:
|
||||
tokensToUpdate = append(tokensToUpdate, x)
|
||||
case <-time.After(10 * time.Second):
|
||||
// wondering what this means?
|
||||
// https://golang.org/ref/spec#Break_statements
|
||||
break AwaitLoop
|
||||
}
|
||||
}
|
||||
|
||||
q, a, _ := sqlx.In("UPDATE tokens SET last_updated = ? WHERE id IN (?)", time.Now().Unix(), tokensToUpdate)
|
||||
|
||||
q = db.Rebind(q)
|
||||
_, err := db.Exec(q, a...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BearerToken parses a Token guiven in the Authorization header, with the
|
||||
// Bearer prefix.
|
||||
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
|
||||
var x struct {
|
||||
Scope string
|
||||
Extra int
|
||||
}
|
||||
db.Get(&x, "SELECT scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
|
||||
if x.Extra == 0 {
|
||||
return common.Token{}, false
|
||||
}
|
||||
|
||||
var privs uint64
|
||||
db.Get(&privs, "SELECT privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
|
||||
|
||||
var t common.Token
|
||||
t.ID = -1
|
||||
t.UserID = x.Extra
|
||||
t.Value = token
|
||||
t.UserPrivileges = common.UserPrivileges(privs)
|
||||
t.TokenPrivileges = oauthPrivileges(x.Scope).CanOnly(t.UserPrivileges)
|
||||
|
||||
return t, true
|
||||
}
|
||||
|
||||
var privilegeMap = map[string]common.Privileges{
|
||||
"read_confidential": common.PrivilegeReadConfidential,
|
||||
"write": common.PrivilegeWrite,
|
||||
}
|
||||
|
||||
func oauthPrivileges(scopes string) common.Privileges {
|
||||
var p common.Privileges
|
||||
for _, x := range strings.Split(scopes, " ") {
|
||||
p |= privilegeMap[x]
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/gin-gonic/gin"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type response404 struct {
|
||||
@@ -11,11 +13,17 @@ type response404 struct {
|
||||
}
|
||||
|
||||
// Handle404 handles requests with no implemented handlers.
|
||||
func Handle404(c *gin.Context) {
|
||||
c.IndentedJSON(404, response404{
|
||||
func Handle404(c *fasthttp.RequestCtx) {
|
||||
c.Response.Header.Add("X-Real-404", "yes")
|
||||
data, err := json.MarshalIndent(response404{
|
||||
ResponseBase: common.ResponseBase{
|
||||
Code: 404,
|
||||
},
|
||||
Cats: surpriseMe(),
|
||||
})
|
||||
}, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.SetStatusCode(404)
|
||||
c.Write(data)
|
||||
}
|
||||
|
@@ -3,11 +3,11 @@ package v1
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type singleBadge struct {
|
||||
ID int `json:"id"`
|
||||
ID int `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
@@ -24,11 +24,10 @@ func BadgesGET(md common.MethodData) common.CodeMessager {
|
||||
rows *sql.Rows
|
||||
err error
|
||||
)
|
||||
if md.C.Query("id") != "" {
|
||||
// TODO(howl): ID validation
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ?", md.C.Query("id"))
|
||||
if md.Query("id") != "" {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges WHERE id = ? LIMIT 1", md.Query("id"))
|
||||
} else {
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges")
|
||||
rows, err = md.DB.Query("SELECT id, name, icon FROM badges " + common.Paginate(md.Query("p"), md.Query("l"), 50))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
@@ -49,3 +48,35 @@ func BadgesGET(md common.MethodData) common.CodeMessager {
|
||||
r.ResponseBase.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type badgeMembersData struct {
|
||||
common.ResponseBase
|
||||
Members []userData `json:"members"`
|
||||
}
|
||||
|
||||
// BadgeMembersGET retrieves the people who have a certain badge.
|
||||
func BadgeMembersGET(md common.MethodData) common.CodeMessager {
|
||||
i := common.Int(md.Query("id"))
|
||||
if i == 0 {
|
||||
return ErrMissingField("id")
|
||||
}
|
||||
|
||||
var members badgeMembersData
|
||||
|
||||
err := md.DB.Select(&members.Members, `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM user_badges ub
|
||||
INNER JOIN users ON users.id = ub.user
|
||||
INNER JOIN users_stats ON users_stats.id = ub.user
|
||||
WHERE badge = ?
|
||||
ORDER BY id ASC `+common.Paginate(md.Query("p"), md.Query("l"), 50), i)
|
||||
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
members.Code = 200
|
||||
return members
|
||||
}
|
||||
|
@@ -2,11 +2,17 @@ package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type difficulty struct {
|
||||
STD float64 `json:"std"`
|
||||
Taiko float64 `json:"taiko"`
|
||||
CTB float64 `json:"ctb"`
|
||||
Mania float64 `json:"mania"`
|
||||
}
|
||||
|
||||
type beatmap struct {
|
||||
BeatmapID int `json:"beatmap_id"`
|
||||
BeatmapsetID int `json:"beatmapset_id"`
|
||||
@@ -15,46 +21,12 @@ type beatmap struct {
|
||||
AR float32 `json:"ar"`
|
||||
OD float32 `json:"od"`
|
||||
Difficulty float64 `json:"difficulty"`
|
||||
Diff2 difficulty `json:"difficulty2"` // fuck nyo
|
||||
MaxCombo int `json:"max_combo"`
|
||||
HitLength int `json:"hit_length"`
|
||||
Ranked int `json:"ranked"`
|
||||
RankedStatusFrozen int `json:"ranked_status_frozen"`
|
||||
LatestUpdate time.Time `json:"latest_update"`
|
||||
}
|
||||
|
||||
type beatmapMayOrMayNotExist struct {
|
||||
BeatmapID *int
|
||||
BeatmapsetID *int
|
||||
BeatmapMD5 *string
|
||||
SongName *string
|
||||
AR *float32
|
||||
OD *float32
|
||||
Difficulty *float64
|
||||
MaxCombo *int
|
||||
HitLength *int
|
||||
Ranked *int
|
||||
RankedStatusFrozen *int
|
||||
LatestUpdate *time.Time
|
||||
}
|
||||
|
||||
func (b *beatmapMayOrMayNotExist) toBeatmap() *beatmap {
|
||||
if b == nil || b.BeatmapID == nil {
|
||||
return nil
|
||||
}
|
||||
return &beatmap{
|
||||
BeatmapID: *b.BeatmapID,
|
||||
BeatmapsetID: *b.BeatmapsetID,
|
||||
BeatmapMD5: *b.BeatmapMD5,
|
||||
SongName: *b.SongName,
|
||||
AR: *b.AR,
|
||||
OD: *b.OD,
|
||||
Difficulty: *b.Difficulty,
|
||||
MaxCombo: *b.MaxCombo,
|
||||
HitLength: *b.HitLength,
|
||||
Ranked: *b.Ranked,
|
||||
RankedStatusFrozen: *b.RankedStatusFrozen,
|
||||
LatestUpdate: *b.LatestUpdate,
|
||||
}
|
||||
LatestUpdate common.UnixTimestamp `json:"latest_update"`
|
||||
}
|
||||
|
||||
type beatmapResponse struct {
|
||||
@@ -77,10 +49,10 @@ type beatmapSetStatusData struct {
|
||||
// the beatmap ranked status is frozen. Or freezed. Freezed best meme 2k16
|
||||
func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
|
||||
var req beatmapSetStatusData
|
||||
md.RequestData.Unmarshal(&req)
|
||||
md.Unmarshal(&req)
|
||||
|
||||
var miss []string
|
||||
if req.BeatmapsetID == 0 && req.BeatmapID == 0 {
|
||||
if req.BeatmapsetID <= 0 && req.BeatmapID <= 0 {
|
||||
miss = append(miss, "beatmapset_id or beatmap_id")
|
||||
}
|
||||
if len(miss) != 0 {
|
||||
@@ -110,74 +82,98 @@ func BeatmapSetStatusPOST(md common.MethodData) common.CodeMessager {
|
||||
SET ranked = ?, ranked_status_freezed = ?
|
||||
WHERE beatmapset_id = ?`, req.RankedStatus, req.Frozen, param)
|
||||
|
||||
return getSet(md, param)
|
||||
if req.BeatmapID > 0 {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("bb", req.BeatmapID)
|
||||
} else {
|
||||
md.Ctx.Request.URI().QueryArgs().SetUint("s", req.BeatmapsetID)
|
||||
}
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
// BeatmapGET retrieves a beatmap.
|
||||
func BeatmapGET(md common.MethodData) common.CodeMessager {
|
||||
if md.C.Query("s") == "" && md.C.Query("b") == "" {
|
||||
return common.SimpleResponse(400, "Must pass either querystring param 'b' or 's'")
|
||||
}
|
||||
setID := common.Int(md.C.Query("s"))
|
||||
if setID != 0 {
|
||||
return getSet(md, setID)
|
||||
}
|
||||
beatmapID := common.Int(md.C.Query("b"))
|
||||
beatmapID := common.Int(md.Query("b"))
|
||||
if beatmapID != 0 {
|
||||
return getBeatmap(md, beatmapID)
|
||||
return getBeatmapSingle(md, beatmapID)
|
||||
}
|
||||
return common.SimpleResponse(400, "Please pass either a valid beatmapset ID or a valid beatmap ID")
|
||||
return getMultipleBeatmaps(md)
|
||||
}
|
||||
|
||||
const baseBeatmapSelect = `
|
||||
SELECT
|
||||
beatmap_id, beatmapset_id, beatmap_md5,
|
||||
song_name, ar, od, difficulty, max_combo,
|
||||
song_name, ar, od, difficulty_std, difficulty_taiko,
|
||||
difficulty_ctb, difficulty_mania, max_combo,
|
||||
hit_length, ranked, ranked_status_freezed,
|
||||
latest_update
|
||||
FROM beatmaps
|
||||
`
|
||||
|
||||
func getSet(md common.MethodData, setID int) common.CodeMessager {
|
||||
rows, err := md.DB.Query(baseBeatmapSelect+"WHERE beatmapset_id = ?", setID)
|
||||
func getMultipleBeatmaps(md common.MethodData) common.CodeMessager {
|
||||
sort := common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"beatmapset_id",
|
||||
"beatmap_id",
|
||||
"id",
|
||||
"ar",
|
||||
"od",
|
||||
"difficulty_std",
|
||||
"difficulty_taiko",
|
||||
"difficulty_ctb",
|
||||
"difficulty_mania",
|
||||
"max_combo",
|
||||
"latest_update",
|
||||
"playcount",
|
||||
"passcount",
|
||||
},
|
||||
Default: "id DESC",
|
||||
Table: "beatmaps",
|
||||
})
|
||||
where := common.
|
||||
Where("beatmap_id = ?", md.Query("bb")).
|
||||
Where("beatmapset_id = ?", md.Query("s")).
|
||||
Where("song_name = ?", md.Query("song_name")).
|
||||
Where("beatmap_md5 = ?", md.Query("md5")).
|
||||
Where("ranked_status_freezed = ?", md.Query("ranked_status_frozen"), "0", "1")
|
||||
|
||||
rows, err := md.DB.Query(baseBeatmapSelect+
|
||||
where.Clause+" "+sort+" "+
|
||||
common.Paginate(md.Query("p"), md.Query("l"), 50), where.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r beatmapSetResponse
|
||||
for rows.Next() {
|
||||
var (
|
||||
b beatmap
|
||||
rawLatestUpdate int64
|
||||
)
|
||||
var b beatmap
|
||||
err = rows.Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Difficulty, &b.MaxCombo,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&rawLatestUpdate,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
b.LatestUpdate = time.Unix(rawLatestUpdate, 0)
|
||||
r.Beatmaps = append(r.Beatmaps, b)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getBeatmap(md common.MethodData, beatmapID int) common.CodeMessager {
|
||||
var (
|
||||
b beatmap
|
||||
rawLatestUpdate int64
|
||||
)
|
||||
func getBeatmapSingle(md common.MethodData, beatmapID int) common.CodeMessager {
|
||||
var b beatmap
|
||||
err := md.DB.QueryRow(baseBeatmapSelect+"WHERE beatmap_id = ? LIMIT 1", beatmapID).Scan(
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Difficulty, &b.MaxCombo,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD, &b.Diff2.Taiko,
|
||||
&b.Diff2.CTB, &b.Diff2.Mania, &b.MaxCombo,
|
||||
&b.HitLength, &b.Ranked, &b.RankedStatusFrozen,
|
||||
&rawLatestUpdate,
|
||||
&b.LatestUpdate,
|
||||
)
|
||||
b.Difficulty = b.Diff2.STD
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "That beatmap could not be found!")
|
||||
@@ -185,7 +181,6 @@ func getBeatmap(md common.MethodData, beatmapID int) common.CodeMessager {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.LatestUpdate = time.Unix(rawLatestUpdate, 0)
|
||||
var r beatmapResponse
|
||||
r.Code = 200
|
||||
r.beatmap = b
|
||||
|
159
app/v1/beatmap_requests.go
Normal file
159
app/v1/beatmap_requests.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/limit"
|
||||
)
|
||||
|
||||
type rankRequestsStatusResponse struct {
|
||||
common.ResponseBase
|
||||
QueueSize int `json:"queue_size"`
|
||||
MaxPerUser int `json:"max_per_user"`
|
||||
Submitted int `json:"submitted"`
|
||||
SubmittedByUser *int `json:"submitted_by_user,omitempty"`
|
||||
CanSubmit *bool `json:"can_submit,omitempty"`
|
||||
NextExpiration *time.Time `json:"next_expiration"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsStatusGET gets the current status for beatmap ranking requests.
|
||||
func BeatmapRankRequestsStatusGET(md common.MethodData) common.CodeMessager {
|
||||
c := common.GetConf()
|
||||
rows, err := md.DB.Query("SELECT userid, time FROM rank_requests WHERE time > ? ORDER BY id ASC LIMIT "+strconv.Itoa(c.RankQueueSize), time.Now().Add(-time.Hour*24).Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r rankRequestsStatusResponse
|
||||
// if it's not auth-free access and we have got ReadConfidential, we can
|
||||
// know if this user can submit beatmaps or not.
|
||||
hasConfid := md.ID() != 0 && md.User.TokenPrivileges&common.PrivilegeReadConfidential > 0
|
||||
if hasConfid {
|
||||
r.SubmittedByUser = new(int)
|
||||
}
|
||||
isFirst := true
|
||||
for rows.Next() {
|
||||
var (
|
||||
user int
|
||||
timestamp common.UnixTimestamp
|
||||
)
|
||||
err := rows.Scan(&user, ×tamp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
// if the user submitted this rank request, increase the number of
|
||||
// rank requests submitted by this user
|
||||
if user == md.ID() && r.SubmittedByUser != nil {
|
||||
(*r.SubmittedByUser)++
|
||||
}
|
||||
// also, if this is the first result, it means it will be the next to
|
||||
// expire.
|
||||
if isFirst {
|
||||
x := time.Time(timestamp)
|
||||
r.NextExpiration = &x
|
||||
isFirst = false
|
||||
}
|
||||
r.Submitted++
|
||||
}
|
||||
r.QueueSize = c.RankQueueSize
|
||||
r.MaxPerUser = c.BeatmapRequestsPerUser
|
||||
if hasConfid {
|
||||
x := r.Submitted < r.QueueSize && *r.SubmittedByUser < r.MaxPerUser
|
||||
r.CanSubmit = &x
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type submitRequestData struct {
|
||||
ID int `json:"id"`
|
||||
SetID int `json:"set_id"`
|
||||
}
|
||||
|
||||
// BeatmapRankRequestsSubmitPOST submits a new beatmap for ranking approval.
|
||||
func BeatmapRankRequestsSubmitPOST(md common.MethodData) common.CodeMessager {
|
||||
var d submitRequestData
|
||||
err := md.Unmarshal(&d)
|
||||
if err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
// check json data is present
|
||||
if d.ID == 0 && d.SetID == 0 {
|
||||
return ErrMissingField("id|set_id")
|
||||
}
|
||||
|
||||
// you've been rate limited
|
||||
if !limit.NonBlockingRequest("rankrequest:u:"+strconv.Itoa(md.ID()), 5) {
|
||||
return common.SimpleResponse(429, "You may only try to request 5 beatmaps per minute.")
|
||||
}
|
||||
|
||||
// find out from BeatmapRankRequestsStatusGET if we can submit beatmaps.
|
||||
statusRaw := BeatmapRankRequestsStatusGET(md)
|
||||
status, ok := statusRaw.(rankRequestsStatusResponse)
|
||||
if !ok {
|
||||
// if it's not a rankRequestsStatusResponse, it means it's an error
|
||||
return statusRaw
|
||||
}
|
||||
if !*status.CanSubmit {
|
||||
return common.SimpleResponse(403, "It's not possible to do a rank request at this time.")
|
||||
}
|
||||
|
||||
w := common.
|
||||
Where("beatmap_id = ?", strconv.Itoa(d.ID)).Or().
|
||||
Where("beatmapset_id = ?", strconv.Itoa(d.SetID))
|
||||
|
||||
var ranked int
|
||||
err = md.DB.QueryRow("SELECT ranked FROM beatmaps "+w.Clause+" LIMIT 1", w.Params...).Scan(&ranked)
|
||||
if ranked >= 2 {
|
||||
return common.SimpleResponse(406, "That beatmap is already ranked.")
|
||||
}
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
// move on
|
||||
case sql.ErrNoRows:
|
||||
data, _ := json.Marshal(d)
|
||||
md.R.Publish("lets:beatmap_updates", string(data))
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// type and value of beatmap rank request
|
||||
t := "b"
|
||||
v := d.ID
|
||||
if d.SetID != 0 {
|
||||
t = "s"
|
||||
v = d.SetID
|
||||
}
|
||||
err = md.DB.QueryRow("SELECT 1 FROM rank_requests WHERE bid = ? AND type = ? AND time > ?",
|
||||
v, t, time.Now().Add(-time.Hour*24).Unix()).Scan(new(int))
|
||||
|
||||
// error handling
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
break
|
||||
case nil:
|
||||
// we're returning a success because if the request was already sent in the past 24
|
||||
// hours, it's as if the user submitted it.
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
default:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
_, err = md.DB.Exec(
|
||||
"INSERT INTO rank_requests (userid, bid, type, time, blacklisted) VALUES (?, ?, ?, ?, 0)",
|
||||
md.ID(), v, t, time.Now().Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
return BeatmapRankRequestsStatusGET(md)
|
||||
}
|
184
app/v1/blog.go
Normal file
184
app/v1/blog.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// This basically proxies requests from Medium's API and is used on Ripple's
|
||||
// home page to display the latest blog posts.
|
||||
|
||||
type mediumResp struct {
|
||||
Success bool `json:"success"`
|
||||
Payload struct {
|
||||
Posts []mediumPost `json:"posts"`
|
||||
References struct {
|
||||
User map[string]mediumUser
|
||||
} `json:"references"`
|
||||
} `json:"payload"`
|
||||
}
|
||||
|
||||
type mediumPost struct {
|
||||
ID string `json:"id"`
|
||||
CreatorID string `json:"creatorId"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt int64 `json:"createdAt"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
Virtuals mediumPostVirtuals `json:"virtuals"`
|
||||
ImportedURL string `json:"importedUrl"`
|
||||
UniqueSlug string `json:"uniqueSlug"`
|
||||
}
|
||||
|
||||
type mediumUser struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type mediumPostVirtuals struct {
|
||||
Subtitle string `json:"subtitle"`
|
||||
WordCount int `json:"wordCount"`
|
||||
ReadingTime float64 `json:"readingTime"`
|
||||
}
|
||||
|
||||
// there's gotta be a better way
|
||||
|
||||
type blogPost struct {
|
||||
ID string `json:"id"`
|
||||
Creator blogUser `json:"creator"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ImportedURL string `json:"imported_url"`
|
||||
UniqueSlug string `json:"unique_slug"`
|
||||
|
||||
Snippet string `json:"snippet"`
|
||||
WordCount int `json:"word_count"`
|
||||
ReadingTime float64 `json:"reading_time"`
|
||||
}
|
||||
|
||||
type blogUser struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type blogPostsResponse struct {
|
||||
common.ResponseBase
|
||||
Posts []blogPost `json:"posts"`
|
||||
}
|
||||
|
||||
// consts for the medium API
|
||||
const (
|
||||
mediumAPIResponsePrefix = `])}while(1);</x>`
|
||||
mediumAPIAllPosts = `https://blog.ripple.moe/latest?format=json`
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register([]blogPost{})
|
||||
}
|
||||
|
||||
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
|
||||
func BlogPostsGET(md common.MethodData) common.CodeMessager {
|
||||
// check if posts are cached in redis
|
||||
res := md.R.Get("api:blog_posts").Val()
|
||||
if res != "" {
|
||||
// decode values
|
||||
posts := make([]blogPost, 0, 20)
|
||||
err := gob.NewDecoder(strings.NewReader(res)).Decode(&posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create response and return
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
// get data from medium api
|
||||
resp, err := http.Get(mediumAPIAllPosts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
// read body and trim the prefix
|
||||
all, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
all = bytes.TrimPrefix(all, []byte(mediumAPIResponsePrefix))
|
||||
|
||||
// unmarshal into response struct
|
||||
var mResp mediumResp
|
||||
err = json.Unmarshal(all, &mResp)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
if !mResp.Success {
|
||||
md.Err(errors.New("medium api call is not successful"))
|
||||
return Err500
|
||||
}
|
||||
|
||||
// create posts slice and fill it up with converted posts from the medium
|
||||
// API
|
||||
posts := make([]blogPost, len(mResp.Payload.Posts))
|
||||
for idx, mp := range mResp.Payload.Posts {
|
||||
var p blogPost
|
||||
|
||||
// convert structs
|
||||
p.ID = mp.ID
|
||||
p.Title = mp.Title
|
||||
p.CreatedAt = time.Unix(0, mp.CreatedAt*1000000)
|
||||
p.UpdatedAt = time.Unix(0, mp.UpdatedAt*1000000)
|
||||
p.ImportedURL = mp.ImportedURL
|
||||
p.UniqueSlug = mp.UniqueSlug
|
||||
|
||||
cr := mResp.Payload.References.User[mp.CreatorID]
|
||||
p.Creator.UserID = cr.UserID
|
||||
p.Creator.Name = cr.Name
|
||||
p.Creator.Username = cr.Username
|
||||
|
||||
p.Snippet = mp.Virtuals.Subtitle
|
||||
p.WordCount = mp.Virtuals.WordCount
|
||||
p.ReadingTime = mp.Virtuals.ReadingTime
|
||||
|
||||
posts[idx] = p
|
||||
}
|
||||
|
||||
// save in redis
|
||||
bb := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(bb).Encode(posts)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
md.R.Set("api:blog_posts", bb.Bytes(), time.Minute*5)
|
||||
|
||||
var r blogPostsResponse
|
||||
r.Code = 200
|
||||
r.Posts = blogLimit(posts, md.Query("l"))
|
||||
return r
|
||||
}
|
||||
|
||||
func blogLimit(posts []blogPost, s string) []blogPost {
|
||||
i := common.Int(s)
|
||||
if i >= len(posts) || i < 1 {
|
||||
return posts
|
||||
}
|
||||
return posts[:i]
|
||||
}
|
@@ -1,92 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type docFile struct {
|
||||
ID int `json:"id"`
|
||||
DocName string `json:"doc_name"`
|
||||
Public bool `json:"public"`
|
||||
IsRule bool `json:"is_rule"`
|
||||
}
|
||||
|
||||
type docResponse struct {
|
||||
common.ResponseBase
|
||||
Files []docFile `json:"files"`
|
||||
}
|
||||
|
||||
// DocGET retrieves a list of documentation files.
|
||||
func DocGET(md common.MethodData) common.CodeMessager {
|
||||
var wc string
|
||||
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" {
|
||||
wc = "WHERE public = '1'"
|
||||
}
|
||||
rows, err := md.DB.Query("SELECT id, doc_name, public, is_rule FROM docs " + wc)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r docResponse
|
||||
for rows.Next() {
|
||||
var f docFile
|
||||
err := rows.Scan(&f.ID, &f.DocName, &f.Public, &f.IsRule)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Files = append(r.Files, f)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type docContentResponse struct {
|
||||
common.ResponseBase
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// DocContentGET retrieves the raw markdown file of a doc file
|
||||
func DocContentGET(md common.MethodData) common.CodeMessager {
|
||||
docID := common.Int(md.C.Query("id"))
|
||||
if docID == 0 {
|
||||
return common.SimpleResponse(404, "Documentation file not found!")
|
||||
}
|
||||
var wc string
|
||||
if !md.User.Privileges.HasPrivilegeBlog() || md.C.Query("public") == "1" {
|
||||
wc = "AND public = '1'"
|
||||
}
|
||||
var r docContentResponse
|
||||
err := md.DB.QueryRow("SELECT doc_contents FROM docs WHERE id = ? "+wc+" LIMIT 1", docID).Scan(&r.Content)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
r.Code = 404
|
||||
r.Message = "Documentation file not found!"
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
default:
|
||||
r.Code = 200
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// DocRulesGET gets the rules.
|
||||
func DocRulesGET(md common.MethodData) common.CodeMessager {
|
||||
var r docContentResponse
|
||||
err := md.DB.QueryRow("SELECT doc_contents FROM docs WHERE is_rule = '1' LIMIT 1").Scan(&r.Content)
|
||||
const ruleFree = "# This Ripple instance is rule-free! Yay!"
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
r.Content = ruleFree
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
case len(r.Content) == 0:
|
||||
r.Content = ruleFree
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
@@ -3,7 +3,7 @@ package v1
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// Boilerplate errors
|
||||
@@ -16,6 +16,6 @@ var (
|
||||
func ErrMissingField(missingFields ...string) common.CodeMessager {
|
||||
return common.ResponseBase{
|
||||
Code: 422, // http://stackoverflow.com/a/10323055/5328069
|
||||
Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".",
|
||||
Message: "Missing parameters: " + strings.Join(missingFields, ", ") + ".",
|
||||
}
|
||||
}
|
||||
|
@@ -2,9 +2,8 @@ package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type friendData struct {
|
||||
@@ -40,22 +39,31 @@ func FriendsGET(md common.MethodData) common.CodeMessager {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
// Yes.
|
||||
myFriendsQuery := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.rank, users.latest_activity,
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka,
|
||||
users_stats.country, users_stats.show_country
|
||||
users_stats.country
|
||||
FROM users_relationships
|
||||
LEFT JOIN users
|
||||
ON users_relationships.user2 = users.id
|
||||
LEFT JOIN users_stats
|
||||
ON users_relationships.user2=users_stats.id
|
||||
WHERE users_relationships.user1=?
|
||||
ORDER BY users_relationships.id`
|
||||
`
|
||||
|
||||
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.C.Query("p"), md.C.Query("l"), 50), md.ID())
|
||||
myFriendsQuery += common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"latest_activity",
|
||||
},
|
||||
Default: "users.id asc",
|
||||
Table: "users",
|
||||
}) + "\n"
|
||||
|
||||
results, err := md.DB.Query(myFriendsQuery+common.Paginate(md.Query("p"), md.Query("l"), 100), md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
@@ -87,23 +95,12 @@ ORDER BY users_relationships.id`
|
||||
func friendPuts(md common.MethodData, row *sql.Rows) (user friendData) {
|
||||
var err error
|
||||
|
||||
registeredOn := int64(0)
|
||||
latestActivity := int64(0)
|
||||
var showcountry bool
|
||||
err = row.Scan(&user.ID, &user.Username, ®isteredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showcountry)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.RegisteredOn, &user.Privileges, &user.LatestActivity, &user.UsernameAKA, &user.Country)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return
|
||||
}
|
||||
|
||||
user.RegisteredOn = time.Unix(registeredOn, 0)
|
||||
user.LatestActivity = time.Unix(latestActivity, 0)
|
||||
|
||||
// If the user wants to stay anonymous, don't show their country.
|
||||
// This can be overriden if we have the ReadConfidential privilege and the user we are accessing is the token owner.
|
||||
if !(showcountry || (md.User.Privileges.HasPrivilegeReadConfidential() && user.ID == md.ID())) {
|
||||
user.Country = "XX"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +114,7 @@ type friendsWithResponse struct {
|
||||
func FriendsWithGET(md common.MethodData) common.CodeMessager {
|
||||
var r friendsWithResponse
|
||||
r.Code = 200
|
||||
uid := common.Int(md.C.Query("id"))
|
||||
uid := common.Int(md.Query("id"))
|
||||
if uid == 0 {
|
||||
return r
|
||||
}
|
||||
@@ -132,9 +129,13 @@ func FriendsWithGET(md common.MethodData) common.CodeMessager {
|
||||
return r
|
||||
}
|
||||
|
||||
// FriendsAddGET is the GET version of FriendsAddPOST.
|
||||
func FriendsAddGET(md common.MethodData) common.CodeMessager {
|
||||
return addFriend(md, common.Int(md.C.Query("id")))
|
||||
// FriendsAddPOST adds an user to the friends.
|
||||
func FriendsAddPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return addFriend(md, u.User)
|
||||
}
|
||||
|
||||
func addFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
@@ -169,16 +170,21 @@ func addFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
|
||||
// userExists makes sure an user exists.
|
||||
func userExists(md common.MethodData, u int) (r bool) {
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND users.allowed='1')", u).Scan(&r)
|
||||
err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND "+
|
||||
md.User.OnlyUserPublic(true)+")", u).Scan(&r)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
md.Err(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FriendsDelGET is the GET version of FriendDelPOST.
|
||||
func FriendsDelGET(md common.MethodData) common.CodeMessager {
|
||||
return delFriend(md, common.Int(md.C.Query("id")))
|
||||
// FriendsDelPOST deletes an user's friend.
|
||||
func FriendsDelPOST(md common.MethodData) common.CodeMessager {
|
||||
var u struct {
|
||||
User int `json:"user"`
|
||||
}
|
||||
md.Unmarshal(&u)
|
||||
return delFriend(md, u.User)
|
||||
}
|
||||
|
||||
func delFriend(md common.MethodData, u int) common.CodeMessager {
|
||||
|
@@ -1,5 +0,0 @@
|
||||
package v1
|
||||
|
||||
func init() {
|
||||
go removeUseless()
|
||||
}
|
@@ -2,9 +2,15 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
redis "gopkg.in/redis.v5"
|
||||
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type leaderboardUser struct {
|
||||
@@ -21,58 +27,95 @@ type leaderboardResponse struct {
|
||||
|
||||
const lbUserQuery = `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.rank, users.latest_activity,
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.country, users_stats.show_country,
|
||||
users_stats.username_aka, users_stats.country,
|
||||
users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.ranked_score_%[1]s, users_stats.total_score_%[1]s, users_stats.playcount_%[1]s,
|
||||
users_stats.replays_watched_%[1]s, users_stats.total_hits_%[1]s,
|
||||
users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s, leaderboard_%[1]s.position as %[1]s_position
|
||||
FROM leaderboard_%[1]s
|
||||
INNER JOIN users ON users.id = leaderboard_%[1]s.user
|
||||
INNER JOIN users_stats ON users_stats.id = leaderboard_%[1]s.user
|
||||
%[2]s`
|
||||
users_stats.avg_accuracy_%[1]s, users_stats.pp_%[1]s
|
||||
FROM users
|
||||
INNER JOIN users_stats ON users_stats.id = users.id
|
||||
WHERE users.id IN (?)
|
||||
`
|
||||
|
||||
// LeaderboardGET gets the leaderboard.
|
||||
func LeaderboardGET(md common.MethodData) common.CodeMessager {
|
||||
m := getMode(md.C.Query("mode"))
|
||||
query := fmt.Sprintf(lbUserQuery, m, `WHERE users.allowed = '1' ORDER BY leaderboard_`+m+`.position `+
|
||||
common.Paginate(md.C.Query("p"), md.C.Query("l"), 100))
|
||||
rows, err := md.DB.Query(query)
|
||||
m := getMode(md.Query("mode"))
|
||||
|
||||
// md.Query.Country
|
||||
p := common.Int(md.Query("p")) - 1
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
l := common.InString(1, md.Query("l"), 500, 50)
|
||||
|
||||
key := "ripple:leaderboard:" + m
|
||||
if md.Query("country") != "" {
|
||||
key += ":" + md.Query("country")
|
||||
}
|
||||
|
||||
results, err := md.R.ZRevRange(key, int64(p*l), int64(p*l+l-1)).Result()
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var resp leaderboardResponse
|
||||
for rows.Next() {
|
||||
var (
|
||||
u leaderboardUser
|
||||
register int64
|
||||
latestActivity int64
|
||||
showCountry bool
|
||||
)
|
||||
err := rows.Scan(
|
||||
&u.ID, &u.Username, ®ister, &u.Rank, &latestActivity,
|
||||
|
||||
&u.UsernameAKA, &u.Country, &showCountry,
|
||||
&u.PlayStyle, &u.FavouriteMode,
|
||||
var resp leaderboardResponse
|
||||
resp.Code = 200
|
||||
|
||||
if len(results) == 0 {
|
||||
return resp
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(lbUserQuery+` ORDER BY users_stats.pp_%[1]s DESC, users_stats.ranked_score_%[1]s DESC`, m)
|
||||
query, params, _ := sqlx.In(query, results)
|
||||
rows, err := md.DB.Query(query, params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
for rows.Next() {
|
||||
var u leaderboardUser
|
||||
err := rows.Scan(
|
||||
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges, &u.LatestActivity,
|
||||
|
||||
&u.UsernameAKA, &u.Country, &u.PlayStyle, &u.FavouriteMode,
|
||||
|
||||
&u.ChosenMode.RankedScore, &u.ChosenMode.TotalScore, &u.ChosenMode.PlayCount,
|
||||
&u.ChosenMode.ReplaysWatched, &u.ChosenMode.TotalHits,
|
||||
&u.ChosenMode.Accuracy, &u.ChosenMode.PP, &u.ChosenMode.GlobalLeaderboardRank,
|
||||
&u.ChosenMode.Accuracy, &u.ChosenMode.PP,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
if !showCountry {
|
||||
u.Country = "XX"
|
||||
u.ChosenMode.Level = ocl.GetLevelPrecise(int64(u.ChosenMode.TotalScore))
|
||||
if i := leaderboardPosition(md.R, m, u.ID); i != nil {
|
||||
u.ChosenMode.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, m, u.ID, u.Country); i != nil {
|
||||
u.ChosenMode.CountryLeaderboardRank = i
|
||||
}
|
||||
u.RegisteredOn = time.Unix(register, 0)
|
||||
u.LatestActivity = time.Unix(latestActivity, 0)
|
||||
resp.Users = append(resp.Users, u)
|
||||
}
|
||||
resp.Code = 200
|
||||
return resp
|
||||
}
|
||||
|
||||
func leaderboardPosition(r *redis.Client, mode string, user int) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode, user)
|
||||
}
|
||||
|
||||
func countryPosition(r *redis.Client, mode string, user int, country string) *int {
|
||||
return _position(r, "ripple:leaderboard:"+mode+":"+strings.ToLower(country), user)
|
||||
}
|
||||
|
||||
func _position(r *redis.Client, key string, user int) *int {
|
||||
res := r.ZRevRank(key, strconv.Itoa(user))
|
||||
if res.Err() == redis.Nil {
|
||||
return nil
|
||||
}
|
||||
x := int(res.Val()) + 1
|
||||
return &x
|
||||
}
|
||||
|
@@ -1,61 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type failedAttempt struct {
|
||||
attempt time.Time
|
||||
ID int
|
||||
}
|
||||
|
||||
var failedAttempts []failedAttempt
|
||||
var failedAttemptsMutex = new(sync.RWMutex)
|
||||
|
||||
// removeUseless removes the expired attempts in failedAttempts
|
||||
func removeUseless() {
|
||||
for {
|
||||
failedAttemptsMutex.RLock()
|
||||
var localCopy = make([]failedAttempt, len(failedAttempts))
|
||||
copy(localCopy, failedAttempts)
|
||||
failedAttemptsMutex.RUnlock()
|
||||
var newStartFrom int
|
||||
for k, v := range localCopy {
|
||||
if time.Since(v.attempt) > time.Minute*10 {
|
||||
newStartFrom = k + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
copySl := localCopy[newStartFrom:]
|
||||
failedAttemptsMutex.Lock()
|
||||
failedAttempts = make([]failedAttempt, len(copySl))
|
||||
for i, v := range copySl {
|
||||
failedAttempts[i] = v
|
||||
}
|
||||
failedAttemptsMutex.Unlock()
|
||||
time.Sleep(time.Minute * 10)
|
||||
}
|
||||
}
|
||||
|
||||
func addFailedAttempt(uid int) {
|
||||
failedAttemptsMutex.Lock()
|
||||
failedAttempts = append(failedAttempts, failedAttempt{
|
||||
attempt: time.Now(),
|
||||
ID: uid,
|
||||
})
|
||||
failedAttemptsMutex.Unlock()
|
||||
}
|
||||
|
||||
func nFailedAttempts(uid int) int {
|
||||
var count int
|
||||
failedAttemptsMutex.RLock()
|
||||
for _, i := range failedAttempts {
|
||||
if i.ID == uid && time.Since(i.attempt) < time.Minute*10 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
failedAttemptsMutex.RUnlock()
|
||||
return count
|
||||
}
|
@@ -3,7 +3,7 @@ package v1
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type setAllowedData struct {
|
||||
@@ -14,30 +14,35 @@ type setAllowedData struct {
|
||||
// UserManageSetAllowedPOST allows to set the allowed status of an user.
|
||||
func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager {
|
||||
data := setAllowedData{}
|
||||
if err := md.RequestData.Unmarshal(&data); err != nil {
|
||||
if err := md.Unmarshal(&data); err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
if data.Allowed < 0 || data.Allowed > 2 {
|
||||
return common.SimpleResponse(400, "Allowed status must be between 0 and 2")
|
||||
}
|
||||
var banDatetime int64
|
||||
var privsSet string
|
||||
if data.Allowed == 0 {
|
||||
banDatetime = time.Now().Unix()
|
||||
privsSet = "privileges = (privileges & ~3)"
|
||||
} else {
|
||||
banDatetime = 0
|
||||
privsSet = "privileges = (privileges | 3)"
|
||||
}
|
||||
_, err := md.DB.Exec("UPDATE users SET allowed = ?, ban_datetime = ? WHERE id = ?", data.Allowed, banDatetime, data.UserID)
|
||||
_, err := md.DB.Exec("UPDATE users SET "+privsSet+", ban_datetime = ? WHERE id = ?", banDatetime, data.UserID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
go fixPrivileges(data.UserID, md.DB)
|
||||
query := `
|
||||
SELECT users.id, users.username, register_datetime, rank,
|
||||
SELECT users.id, users.username, register_datetime, privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country, users_stats.show_country
|
||||
users_stats.country
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE users.id=?
|
||||
LIMIT 1`
|
||||
return userPuts(md, md.DB.QueryRow(query, data.UserID))
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, data.UserID))
|
||||
}
|
||||
|
@@ -1,3 +1,7 @@
|
||||
// +build !windows
|
||||
|
||||
// TODO: Make all these methods POST
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
@@ -10,7 +14,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
@@ -78,7 +82,7 @@ func MetaUpdateGET(md common.MethodData) common.CodeMessager {
|
||||
if !execCommand("go", "get", "-v", "-u", "-d") {
|
||||
return
|
||||
}
|
||||
if !execCommand("go", "build", "-v", "-o", "api") {
|
||||
if !execCommand("bash", "-c", "go build -v -ldflags \"-X main.Version=`git rev-parse HEAD`\"") {
|
||||
return
|
||||
}
|
||||
|
42
app/v1/meta_windows.go
Normal file
42
app/v1/meta_windows.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// +build windows
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
// MetaRestartGET restarts the API with Zero Downtime™.
|
||||
func MetaRestartGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "brb in your dreams")
|
||||
}
|
||||
|
||||
// MetaKillGET kills the API process. NOTE TO EVERYONE: NEVER. EVER. USE IN PROD.
|
||||
// Mainly created because I couldn't bother to fire up a terminal, do htop and kill the API each time.
|
||||
func MetaKillGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "haha")
|
||||
}
|
||||
|
||||
var upSince = time.Now()
|
||||
|
||||
type metaUpSinceResponse struct {
|
||||
common.ResponseBase
|
||||
Code int `json:"code"`
|
||||
Since int64 `json:"since"`
|
||||
}
|
||||
|
||||
// MetaUpSinceGET retrieves the moment the API application was started.
|
||||
// Mainly used to get if the API was restarted.
|
||||
func MetaUpSinceGET(md common.MethodData) common.CodeMessager {
|
||||
return metaUpSinceResponse{
|
||||
Code: 200,
|
||||
Since: int64(upSince.UnixNano()),
|
||||
}
|
||||
}
|
||||
|
||||
// MetaUpdateGET updates the API to the latest version, and restarts it.
|
||||
func MetaUpdateGET(md common.MethodData) common.CodeMessager {
|
||||
return common.SimpleResponse(200, "lol u wish")
|
||||
}
|
@@ -4,7 +4,7 @@ import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
var rn = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
@@ -79,16 +79,26 @@ var randomSentences = [...]string{
|
||||
"Superman dies",
|
||||
"PP when?",
|
||||
"RWC hype",
|
||||
"I'd just like to interject for a moment.",
|
||||
"Running on an apple pie!",
|
||||
":thinking:",
|
||||
"The total entropy of an isolated system can only increase over time",
|
||||
"Where are my testicles, Summer?",
|
||||
"Why don't you ask the smartest people in the universe? Oh yeah, you can't. They blew up.",
|
||||
}
|
||||
|
||||
func surpriseMe() string {
|
||||
return randomSentences[rn.Intn(len(randomSentences))] + " " + kaomojis[rn.Intn(len(kaomojis))]
|
||||
n := int(time.Now().UnixNano())
|
||||
return randomSentences[n%len(randomSentences)] + " " + kaomojis[n%len(kaomojis)]
|
||||
}
|
||||
|
||||
type pingResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"user_id"`
|
||||
Privileges int `json:"privileges"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
UserPrivileges common.UserPrivileges `json:"user_privileges"`
|
||||
PrivilegesS string `json:"privileges_string"`
|
||||
UserPrivilegesS string `json:"user_privileges_string"`
|
||||
}
|
||||
|
||||
// PingGET is a message to check with the API that we are logged in, and know what are our privileges.
|
||||
@@ -97,13 +107,16 @@ func PingGET(md common.MethodData) common.CodeMessager {
|
||||
r.Code = 200
|
||||
|
||||
if md.ID() == 0 {
|
||||
r.Message = "You have not given us a token, so we don't know who you are! But you can still login with /api/v1/tokens/new " + kaomojis[rn.Intn(len(kaomojis))]
|
||||
r.Message = "You have not given us a token, so we don't know who you are! But you can still login with POST /tokens " + kaomojis[rn.Intn(len(kaomojis))]
|
||||
} else {
|
||||
r.Message = surpriseMe()
|
||||
}
|
||||
|
||||
r.ID = md.ID()
|
||||
r.Privileges = int(md.User.Privileges)
|
||||
r.Privileges = md.User.TokenPrivileges
|
||||
r.UserPrivileges = md.User.UserPrivileges
|
||||
r.PrivilegesS = md.User.TokenPrivileges.String()
|
||||
r.UserPrivilegesS = md.User.UserPrivileges.String()
|
||||
|
||||
return r
|
||||
}
|
||||
|
@@ -1,43 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type privilegesData struct {
|
||||
common.ResponseBase
|
||||
Read bool `json:"read"`
|
||||
ReadConfidential bool `json:"read_confidential"`
|
||||
Write bool `json:"write"`
|
||||
ManageBadges bool `json:"manage_badges"`
|
||||
BetaKeys bool `json:"beta_keys"`
|
||||
ManageSettings bool `json:"manage_settings"`
|
||||
ViewUserAdvanced bool `json:"view_user_advanced"`
|
||||
ManageUser bool `json:"manage_user"`
|
||||
ManageRoles bool `json:"manage_roles"`
|
||||
ManageAPIKeys bool `json:"manage_api_keys"`
|
||||
Blog bool `json:"blog"`
|
||||
APIMeta bool `json:"api_meta"`
|
||||
Beatmap bool `json:"beatmap"`
|
||||
}
|
||||
|
||||
// PrivilegesGET returns an explaination for the privileges, telling the client what they can do with this token.
|
||||
func PrivilegesGET(md common.MethodData) common.CodeMessager {
|
||||
r := privilegesData{}
|
||||
r.Code = 200
|
||||
// This code sucks.
|
||||
r.Read = md.User.Privileges.HasPrivilegeRead()
|
||||
r.ReadConfidential = md.User.Privileges.HasPrivilegeReadConfidential()
|
||||
r.Write = md.User.Privileges.HasPrivilegeWrite()
|
||||
r.ManageBadges = md.User.Privileges.HasPrivilegeManageBadges()
|
||||
r.BetaKeys = md.User.Privileges.HasPrivilegeBetaKeys()
|
||||
r.ManageSettings = md.User.Privileges.HasPrivilegeManageSettings()
|
||||
r.ViewUserAdvanced = md.User.Privileges.HasPrivilegeViewUserAdvanced()
|
||||
r.ManageUser = md.User.Privileges.HasPrivilegeManageUser()
|
||||
r.ManageRoles = md.User.Privileges.HasPrivilegeManageRoles()
|
||||
r.ManageAPIKeys = md.User.Privileges.HasPrivilegeManageAPIKeys()
|
||||
r.Blog = md.User.Privileges.HasPrivilegeBlog()
|
||||
r.APIMeta = md.User.Privileges.HasPrivilegeAPIMeta()
|
||||
r.Beatmap = md.User.Privileges.HasPrivilegeBeatmap()
|
||||
return r
|
||||
}
|
156
app/v1/score.go
Normal file
156
app/v1/score.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
// Score is a score done on Ripple.
|
||||
type Score struct {
|
||||
ID int `json:"id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Score int64 `json:"score"`
|
||||
MaxCombo int `json:"max_combo"`
|
||||
FullCombo bool `json:"full_combo"`
|
||||
Mods int `json:"mods"`
|
||||
Count300 int `json:"count_300"`
|
||||
Count100 int `json:"count_100"`
|
||||
Count50 int `json:"count_50"`
|
||||
CountGeki int `json:"count_geki"`
|
||||
CountKatu int `json:"count_katu"`
|
||||
CountMiss int `json:"count_miss"`
|
||||
Time common.UnixTimestamp `json:"time"`
|
||||
PlayMode int `json:"play_mode"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP float32 `json:"pp"`
|
||||
Rank string `json:"rank"`
|
||||
Completed int `json:"completed"`
|
||||
}
|
||||
|
||||
// beatmapScore is to differentiate from userScore, as beatmapScore contains
|
||||
// also an user, while userScore contains the beatmap.
|
||||
type beatmapScore struct {
|
||||
Score
|
||||
User userData `json:"user"`
|
||||
}
|
||||
|
||||
type scoresResponse struct {
|
||||
common.ResponseBase
|
||||
Scores []beatmapScore `json:"scores"`
|
||||
}
|
||||
|
||||
// ScoresGET retrieves the top scores for a certain beatmap.
|
||||
func ScoresGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
beatmapMD5 string
|
||||
r scoresResponse
|
||||
)
|
||||
switch {
|
||||
case md.Query("md5") != "":
|
||||
beatmapMD5 = md.Query("md5")
|
||||
case md.Query("b") != "":
|
||||
err := md.DB.Get(&beatmapMD5, "SELECT beatmap_md5 FROM beatmaps WHERE beatmap_id = ? LIMIT 1", md.Query("b"))
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
r.Code = 200
|
||||
return r
|
||||
case err != nil:
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
default:
|
||||
return ErrMissingField("md5|b")
|
||||
}
|
||||
|
||||
sort := common.Sort(md, common.SortConfiguration{
|
||||
Default: "scores.pp DESC, scores.score DESC",
|
||||
Table: "scores",
|
||||
Allowed: []string{"pp", "score", "accuracy", "id"},
|
||||
})
|
||||
|
||||
rows, err := md.DB.Query(`
|
||||
SELECT
|
||||
scores.id, scores.beatmap_md5, scores.score,
|
||||
scores.max_combo, scores.full_combo, scores.mods,
|
||||
scores.300_count, scores.100_count, scores.50_count,
|
||||
scores.gekis_count, scores.katus_count, scores.misses_count,
|
||||
scores.time, scores.play_mode, scores.accuracy, scores.pp,
|
||||
scores.completed,
|
||||
|
||||
users.id, users.username, users.register_datetime, users.privileges,
|
||||
users.latest_activity, users_stats.username_aka, users_stats.country
|
||||
FROM scores
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
INNER JOIN users_stats ON users_stats.id = scores.userid
|
||||
WHERE scores.beatmap_md5 = ? AND scores.completed = '3' AND `+md.User.OnlyUserPublic(true)+
|
||||
` `+genModeClause(md)+`
|
||||
`+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), beatmapMD5)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
s beatmapScore
|
||||
u userData
|
||||
)
|
||||
err := rows.Scan(
|
||||
&s.ID, &s.BeatmapMD5, &s.Score.Score,
|
||||
&s.MaxCombo, &s.FullCombo, &s.Mods,
|
||||
&s.Count300, &s.Count100, &s.Count50,
|
||||
&s.CountGeki, &s.CountKatu, &s.CountMiss,
|
||||
&s.Time, &s.PlayMode, &s.Accuracy, &s.PP,
|
||||
&s.Completed,
|
||||
|
||||
&u.ID, &u.Username, &u.RegisteredOn, &u.Privileges,
|
||||
&u.LatestActivity, &u.UsernameAKA, &u.Country,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
s.User = u
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(s.PlayMode),
|
||||
osuapi.Mods(s.Mods),
|
||||
s.Accuracy,
|
||||
s.Count300,
|
||||
s.Count100,
|
||||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
r.Scores = append(r.Scores, s)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
func getMode(m string) string {
|
||||
switch m {
|
||||
case "1":
|
||||
return "taiko"
|
||||
case "2":
|
||||
return "ctb"
|
||||
case "3":
|
||||
return "mania"
|
||||
default:
|
||||
return "std"
|
||||
}
|
||||
}
|
||||
|
||||
func genModeClause(md common.MethodData) string {
|
||||
var modeClause string
|
||||
if md.Query("mode") != "" {
|
||||
m, err := strconv.Atoi(md.Query("mode"))
|
||||
if err == nil && m >= 0 && m <= 3 {
|
||||
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
|
||||
}
|
||||
}
|
||||
return modeClause
|
||||
}
|
167
app/v1/self.go
Normal file
167
app/v1/self.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/semantic-icons-ugc"
|
||||
)
|
||||
|
||||
type donorInfoResponse struct {
|
||||
common.ResponseBase
|
||||
HasDonor bool `json:"has_donor"`
|
||||
Expiration common.UnixTimestamp `json:"expiration"`
|
||||
}
|
||||
|
||||
// UsersSelfDonorInfoGET returns information about the users' donor status
|
||||
func UsersSelfDonorInfoGET(md common.MethodData) common.CodeMessager {
|
||||
var r donorInfoResponse
|
||||
var privileges uint64
|
||||
err := md.DB.QueryRow("SELECT privileges, donor_expire FROM users WHERE id = ?", md.ID()).
|
||||
Scan(&privileges, &r.Expiration)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
r.HasDonor = common.UserPrivileges(privileges)&common.UserPrivilegeDonor > 0
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
type favouriteModeResponse struct {
|
||||
common.ResponseBase
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
}
|
||||
|
||||
// UsersSelfFavouriteModeGET gets the current user's favourite mode
|
||||
func UsersSelfFavouriteModeGET(md common.MethodData) common.CodeMessager {
|
||||
var f favouriteModeResponse
|
||||
f.Code = 200
|
||||
if md.ID() == 0 {
|
||||
return f
|
||||
}
|
||||
err := md.DB.QueryRow("SELECT users_stats.favourite_mode FROM users_stats WHERE id = ?", md.ID()).
|
||||
Scan(&f.FavouriteMode)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type userSettingsData struct {
|
||||
UsernameAKA *string `json:"username_aka"`
|
||||
FavouriteMode *int `json:"favourite_mode"`
|
||||
CustomBadge struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
} `json:"custom_badge"`
|
||||
PlayStyle *int `json:"play_style"`
|
||||
}
|
||||
|
||||
// UsersSelfSettingsPOST allows to modify information about the current user.
|
||||
func UsersSelfSettingsPOST(md common.MethodData) common.CodeMessager {
|
||||
var d userSettingsData
|
||||
md.Unmarshal(&d)
|
||||
|
||||
// input sanitisation
|
||||
*d.UsernameAKA = common.SanitiseString(*d.UsernameAKA)
|
||||
if md.User.UserPrivileges&common.UserPrivilegeDonor > 0 {
|
||||
d.CustomBadge.Name = common.SanitiseString(d.CustomBadge.Name)
|
||||
d.CustomBadge.Icon = sanitiseIconName(d.CustomBadge.Icon)
|
||||
} else {
|
||||
d.CustomBadge.singleBadge = singleBadge{}
|
||||
d.CustomBadge.Show = nil
|
||||
}
|
||||
d.FavouriteMode = intPtrIn(0, d.FavouriteMode, 3)
|
||||
|
||||
q := new(common.UpdateQuery).
|
||||
Add("s.username_aka", d.UsernameAKA).
|
||||
Add("s.favourite_mode", d.FavouriteMode).
|
||||
Add("s.custom_badge_name", d.CustomBadge.Name).
|
||||
Add("s.custom_badge_icon", d.CustomBadge.Icon).
|
||||
Add("s.show_custom_badge", d.CustomBadge.Show).
|
||||
Add("s.play_style", d.PlayStyle)
|
||||
_, err := md.DB.Exec("UPDATE users u, users_stats s SET "+q.Fields()+" WHERE s.id = u.id AND u.id = ?", append(q.Parameters, md.ID())...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
return UsersSelfSettingsGET(md)
|
||||
}
|
||||
|
||||
func sanitiseIconName(s string) string {
|
||||
classes := strings.Split(s, " ")
|
||||
n := make([]string, 0, len(classes))
|
||||
for _, c := range classes {
|
||||
if !in(c, n) && in(c, semanticiconsugc.SaneIcons) {
|
||||
n = append(n, c)
|
||||
}
|
||||
}
|
||||
return strings.Join(n, " ")
|
||||
}
|
||||
|
||||
func in(a string, b []string) bool {
|
||||
for _, x := range b {
|
||||
if x == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type userSettingsResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Flags uint `json:"flags"`
|
||||
userSettingsData
|
||||
}
|
||||
|
||||
// UsersSelfSettingsGET allows to get "sensitive" information about the current user.
|
||||
func UsersSelfSettingsGET(md common.MethodData) common.CodeMessager {
|
||||
var r userSettingsResponse
|
||||
var ccb bool
|
||||
r.Code = 200
|
||||
err := md.DB.QueryRow(`
|
||||
SELECT
|
||||
u.id, u.username,
|
||||
u.email, s.username_aka, s.favourite_mode,
|
||||
s.show_custom_badge, s.custom_badge_icon,
|
||||
s.custom_badge_name, s.can_custom_badge,
|
||||
s.play_style, u.flags
|
||||
FROM users u
|
||||
LEFT JOIN users_stats s ON u.id = s.id
|
||||
WHERE u.id = ?`, md.ID()).Scan(
|
||||
&r.ID, &r.Username,
|
||||
&r.Email, &r.UsernameAKA, &r.FavouriteMode,
|
||||
&r.CustomBadge.Show, &r.CustomBadge.Icon,
|
||||
&r.CustomBadge.Name, &ccb,
|
||||
&r.PlayStyle, &r.Flags,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if !ccb {
|
||||
r.CustomBadge = struct {
|
||||
singleBadge
|
||||
Show *bool `json:"show"`
|
||||
}{}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func intPtrIn(x int, y *int, z int) *int {
|
||||
if y == nil {
|
||||
return nil
|
||||
}
|
||||
if *y > z {
|
||||
return nil
|
||||
}
|
||||
if *y < x {
|
||||
return nil
|
||||
}
|
||||
return y
|
||||
}
|
155
app/v1/token.go
155
app/v1/token.go
@@ -2,12 +2,20 @@ package v1
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"git.zxq.co/ripple/schiavolib"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/rippleapi/limit"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
type tokenNewInData struct {
|
||||
@@ -33,11 +41,13 @@ type tokenNewResponse struct {
|
||||
func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
var r tokenNewResponse
|
||||
data := tokenNewInData{}
|
||||
err := md.RequestData.Unmarshal(&data)
|
||||
err := md.Unmarshal(&data)
|
||||
if err != nil {
|
||||
return ErrBadJSON
|
||||
}
|
||||
|
||||
md.Doggo.Incr("tokens.new", nil, 1)
|
||||
|
||||
var miss []string
|
||||
if data.Username == "" && data.UserID == 0 {
|
||||
miss = append(miss, "username|id")
|
||||
@@ -50,21 +60,21 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
}
|
||||
|
||||
var q *sql.Row
|
||||
const base = "SELECT id, username, rank, password_md5, password_version, allowed FROM users "
|
||||
const base = "SELECT id, username, privileges, password_md5, password_version, privileges FROM users "
|
||||
if data.UserID != 0 {
|
||||
q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID)
|
||||
} else {
|
||||
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username)
|
||||
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", common.SafeUsername(data.Username))
|
||||
}
|
||||
|
||||
var (
|
||||
rank int
|
||||
pw string
|
||||
pwVersion int
|
||||
allowed int
|
||||
privilegesRaw uint64
|
||||
)
|
||||
|
||||
err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &allowed)
|
||||
err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &privilegesRaw)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No user with that username/id was found.")
|
||||
@@ -72,8 +82,9 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
privileges := common.UserPrivileges(privilegesRaw)
|
||||
|
||||
if nFailedAttempts(r.ID) > 20 {
|
||||
if !limit.NonBlockingRequest(fmt.Sprintf("loginattempt:%d:%s", r.ID, md.ClientIP()), 5) {
|
||||
return common.SimpleResponse(429, "You've made too many login attempts. Try again later.")
|
||||
}
|
||||
|
||||
@@ -82,19 +93,19 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
go addFailedAttempt(r.ID)
|
||||
return common.SimpleResponse(403, "That password doesn't match!")
|
||||
}
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if allowed == 0 {
|
||||
r.Code = 200
|
||||
const want = (common.UserPrivilegePublic | common.UserPrivilegeNormal)
|
||||
if (privileges & want) != want {
|
||||
r.Code = 402
|
||||
r.Message = "That user is banned."
|
||||
r.Banned = true
|
||||
return r
|
||||
}
|
||||
r.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank))
|
||||
r.Privileges = int(common.Privileges(data.Privileges).CanOnly(privileges))
|
||||
|
||||
var (
|
||||
tokenStr string
|
||||
@@ -104,7 +115,7 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
tokenStr = common.RandomString(32)
|
||||
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
|
||||
r.Token = tokenStr
|
||||
id := 0
|
||||
var id int
|
||||
|
||||
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=? LIMIT 1", tokenMD5).Scan(&id)
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -115,7 +126,8 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
return Err500
|
||||
}
|
||||
}
|
||||
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token, private) VALUES (?, ?, ?, ?, '0')", r.ID, r.Privileges, data.Description, tokenMD5)
|
||||
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token, private, last_updated) VALUES (?, ?, ?, ?, '0', ?)",
|
||||
r.ID, r.Privileges, data.Description, tokenMD5, time.Now().Unix())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
@@ -125,13 +137,19 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
||||
return r
|
||||
}
|
||||
|
||||
// TokenSelfDeleteGET deletes the token the user is connecting with.
|
||||
func TokenSelfDeleteGET(md common.MethodData) common.CodeMessager {
|
||||
// TokenSelfDeletePOST deletes the token the user is connecting with.
|
||||
func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
|
||||
}
|
||||
_, err := md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
|
||||
var err error
|
||||
if md.IsBearer() {
|
||||
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
|
||||
} else {
|
||||
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
|
||||
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
|
||||
}
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
@@ -143,22 +161,23 @@ type token struct {
|
||||
ID int `json:"id"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
Description string `json:"description"`
|
||||
LastUpdated common.UnixTimestamp `json:"last_updated"`
|
||||
}
|
||||
type tokenResponse struct {
|
||||
common.ResponseBase
|
||||
Tokens []token `json:"token"`
|
||||
Tokens []token `json:"tokens"`
|
||||
}
|
||||
|
||||
// TokenGET retrieves a list listing all the user's public tokens.
|
||||
func TokenGET(md common.MethodData) common.CodeMessager {
|
||||
rows, err := md.DB.Query("SELECT id, privileges, description FROM tokens WHERE user = ? AND private = '0'", md.ID())
|
||||
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens WHERE user = ? AND private = '0' "+common.Paginate(md.Query("p"), md.Query("l"), 50), md.ID())
|
||||
if err != nil {
|
||||
return Err500
|
||||
}
|
||||
var r tokenResponse
|
||||
for rows.Next() {
|
||||
var t token
|
||||
err = rows.Scan(&t.ID, &t.Privileges, &t.Description)
|
||||
err = rows.Scan(&t.ID, &t.Privileges, &t.Description, &t.LastUpdated)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
@@ -169,17 +188,67 @@ func TokenGET(md common.MethodData) common.CodeMessager {
|
||||
return r
|
||||
}
|
||||
|
||||
type oauthClient struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OwnerID int `json:"owner_id"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
|
||||
func (o *oauthClient) Scan(src interface{}) error {
|
||||
var s []byte
|
||||
switch x := src.(type) {
|
||||
case string:
|
||||
s = []byte(x)
|
||||
case []byte:
|
||||
s = x
|
||||
default:
|
||||
return errors.New("Can't scan non-string")
|
||||
}
|
||||
|
||||
var vals [3]string
|
||||
err := json.Unmarshal(s, &vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.Name = vals[0]
|
||||
o.OwnerID, _ = strconv.Atoi(vals[1])
|
||||
o.Avatar = vals[2]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bearerToken struct {
|
||||
Client oauthClient `json:"client"`
|
||||
Scope string `json:"scope"`
|
||||
Privileges common.Privileges `json:"privileges"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
type tokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
token
|
||||
}
|
||||
|
||||
type bearerTokenSingleResponse struct {
|
||||
common.ResponseBase
|
||||
bearerToken
|
||||
}
|
||||
|
||||
// TokenSelfGET retrieves information about the token the user is connecting with.
|
||||
func TokenSelfGET(md common.MethodData) common.CodeMessager {
|
||||
if md.ID() == 0 {
|
||||
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
|
||||
}
|
||||
if md.IsBearer() {
|
||||
return getBearerToken(md)
|
||||
}
|
||||
var r tokenSingleResponse
|
||||
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
|
||||
err := md.DB.QueryRow("SELECT id, privileges, description FROM tokens WHERE id = ?", md.User.ID).Scan(
|
||||
&r.ID, &r.Privileges, &r.Description,
|
||||
err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
|
||||
&r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
@@ -189,18 +258,37 @@ func TokenSelfGET(md common.MethodData) common.CodeMessager {
|
||||
return r
|
||||
}
|
||||
|
||||
// TokenFixPrivilegesGET fixes the privileges on the token of the given user,
|
||||
func getBearerToken(md common.MethodData) common.CodeMessager {
|
||||
var b bearerTokenSingleResponse
|
||||
err := md.DB.
|
||||
QueryRow(`
|
||||
SELECT t.scope, t.created_at, c.id, c.extra
|
||||
FROM osin_access t INNER JOIN osin_client c
|
||||
WHERE t.access_token = ?
|
||||
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
|
||||
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
b.Code = 200
|
||||
b.Privileges = md.User.TokenPrivileges
|
||||
return b
|
||||
}
|
||||
|
||||
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
|
||||
// or of all the users if no user is given.
|
||||
func TokenFixPrivilegesGET(md common.MethodData) common.CodeMessager {
|
||||
id := common.Int(md.C.Query("id"))
|
||||
if md.C.Query("id") == "self" {
|
||||
func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
|
||||
id := common.Int(md.Query("id"))
|
||||
if md.Query("id") == "self" {
|
||||
id = md.ID()
|
||||
}
|
||||
go fixPrivileges(id, md.DB)
|
||||
return common.SimpleResponse(200, "Privilege fixing started!")
|
||||
}
|
||||
|
||||
func fixPrivileges(user int, db *sql.DB) {
|
||||
func fixPrivileges(user int, db *sqlx.DB) {
|
||||
var wc string
|
||||
var params = make([]interface{}, 0, 1)
|
||||
if user != 0 {
|
||||
@@ -210,7 +298,7 @@ func fixPrivileges(user int, db *sql.DB) {
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
tokens.id, tokens.privileges, users.rank
|
||||
tokens.id, tokens.privileges, users.privileges
|
||||
FROM tokens
|
||||
LEFT JOIN users ON users.id = tokens.user
|
||||
`+wc, params...)
|
||||
@@ -225,11 +313,16 @@ LEFT JOIN users ON users.id = tokens.user
|
||||
privsRaw uint64
|
||||
privs common.Privileges
|
||||
newPrivs common.Privileges
|
||||
rank int
|
||||
privilegesRaw uint64
|
||||
)
|
||||
rows.Scan(&id, &privsRaw, &rank)
|
||||
err := rows.Scan(&id, &privsRaw, &privilegesRaw)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
privileges := common.UserPrivileges(privilegesRaw)
|
||||
privs = common.Privileges(privsRaw)
|
||||
newPrivs = privs.CanOnly(rank)
|
||||
newPrivs = privs.CanOnly(privileges)
|
||||
if newPrivs != privs {
|
||||
_, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id)
|
||||
if err != nil {
|
||||
|
304
app/v1/user.go
304
app/v1/user.go
@@ -5,56 +5,54 @@ import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.zxq.co/ripple/ocl"
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"zxq.co/ripple/ocl"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
type userData struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
UsernameAKA string `json:"username_aka"`
|
||||
RegisteredOn time.Time `json:"registered_on"`
|
||||
Rank int `json:"rank"`
|
||||
LatestActivity time.Time `json:"latest_activity"`
|
||||
RegisteredOn common.UnixTimestamp `json:"registered_on"`
|
||||
Privileges uint64 `json:"privileges"`
|
||||
LatestActivity common.UnixTimestamp `json:"latest_activity"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
|
||||
const userFields = `SELECT users.id, users.username, register_datetime, users.privileges,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country
|
||||
FROM users
|
||||
INNER JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
`
|
||||
|
||||
// UsersGET is the API handler for GET /users
|
||||
func UsersGET(md common.MethodData) common.CodeMessager {
|
||||
shouldRet, whereClause, param := whereClauseUser(md, "users")
|
||||
if shouldRet != nil {
|
||||
return *shouldRet
|
||||
return userPutsMulti(md)
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT users.id, users.username, register_datetime, rank,
|
||||
latest_activity, users_stats.username_aka,
|
||||
users_stats.country, users_stats.show_country
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
WHERE ` + whereClause + ` AND users.allowed='1'
|
||||
query := userFields + `
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1`
|
||||
return userPuts(md, md.DB.QueryRow(query, param))
|
||||
return userPutsSingle(md, md.DB.QueryRowx(query, param))
|
||||
}
|
||||
|
||||
type userPutsUserData struct {
|
||||
type userPutsSingleUserData struct {
|
||||
common.ResponseBase
|
||||
userData
|
||||
}
|
||||
|
||||
func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager {
|
||||
func userPutsSingle(md common.MethodData, row *sqlx.Row) common.CodeMessager {
|
||||
var err error
|
||||
var user userPutsUserData
|
||||
var user userPutsSingleUserData
|
||||
|
||||
var (
|
||||
registeredOn int64
|
||||
latestActivity int64
|
||||
showCountry bool
|
||||
)
|
||||
err = row.Scan(&user.ID, &user.Username, ®isteredOn, &user.Rank, &latestActivity, &user.UsernameAKA, &user.Country, &showCountry)
|
||||
err = row.StructScan(&user.userData)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.SimpleResponse(404, "No such user was found!")
|
||||
@@ -63,44 +61,91 @@ func userPuts(md common.MethodData, row *sql.Row) common.CodeMessager {
|
||||
return Err500
|
||||
}
|
||||
|
||||
user.RegisteredOn = time.Unix(registeredOn, 0)
|
||||
user.LatestActivity = time.Unix(latestActivity, 0)
|
||||
|
||||
user.Country = genCountry(md, user.ID, showCountry, user.Country)
|
||||
|
||||
user.Code = 200
|
||||
return user
|
||||
}
|
||||
|
||||
func badgesToArray(badges string) []int {
|
||||
var end []int
|
||||
badgesSl := strings.Split(badges, ",")
|
||||
for _, badge := range badgesSl {
|
||||
if badge != "" && badge != "0" {
|
||||
nb := common.Int(badge)
|
||||
if nb != 0 {
|
||||
end = append(end, nb)
|
||||
}
|
||||
}
|
||||
}
|
||||
return end
|
||||
type userPutsMultiUserData struct {
|
||||
common.ResponseBase
|
||||
Users []userData `json:"users"`
|
||||
}
|
||||
|
||||
func genCountry(md common.MethodData, uid int, showCountry bool, country string) string {
|
||||
// If the user wants to stay anonymous, don't show their country.
|
||||
// This can be overriden if we have the ReadConfidential privilege and the user we are accessing is the token owner.
|
||||
if showCountry || (md.User.Privileges.HasPrivilegeReadConfidential() && uid == md.ID()) {
|
||||
return country
|
||||
func userPutsMulti(md common.MethodData) common.CodeMessager {
|
||||
pm := md.Ctx.Request.URI().QueryArgs().PeekMulti
|
||||
// query composition
|
||||
wh := common.
|
||||
Where("users.username_safe = ?", common.SafeUsername(md.Query("nname"))).
|
||||
Where("users.id = ?", md.Query("iid")).
|
||||
Where("users.privileges = ?", md.Query("privileges")).
|
||||
Where("users.privileges & ? > 0", md.Query("has_privileges")).
|
||||
Where("users.privileges & ? = 0", md.Query("has_not_privileges")).
|
||||
Where("users_stats.country = ?", md.Query("country")).
|
||||
Where("users_stats.username_aka = ?", md.Query("name_aka")).
|
||||
Where("privileges_groups.name = ?", md.Query("privilege_group")).
|
||||
In("users.id", pm("ids")...).
|
||||
In("users.username_safe", safeUsernameBulk(pm("names"))...).
|
||||
In("users_stats.username_aka", pm("names_aka")...).
|
||||
In("users_stats.country", pm("countries")...)
|
||||
|
||||
var extraJoin string
|
||||
if md.Query("privilege_group") != "" {
|
||||
extraJoin = " LEFT JOIN privileges_groups ON users.privileges & privileges_groups.privileges = privileges_groups.privileges "
|
||||
}
|
||||
return "XX"
|
||||
|
||||
query := userFields + extraJoin + wh.ClauseSafe() + " AND " + md.User.OnlyUserPublic(true) +
|
||||
" " + common.Sort(md, common.SortConfiguration{
|
||||
Allowed: []string{
|
||||
"id",
|
||||
"username",
|
||||
"privileges",
|
||||
"donor_expire",
|
||||
"latest_activity",
|
||||
"silence_end",
|
||||
},
|
||||
Default: "id ASC",
|
||||
Table: "users",
|
||||
}) +
|
||||
" " + common.Paginate(md.Query("p"), md.Query("l"), 100)
|
||||
|
||||
// query execution
|
||||
rows, err := md.DB.Queryx(query, wh.Params...)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
var r userPutsMultiUserData
|
||||
for rows.Next() {
|
||||
var u userData
|
||||
err := rows.StructScan(&u)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Users = append(r.Users, u)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfGET is a shortcut for /users/id/self. (/users/self)
|
||||
func UserSelfGET(md common.MethodData) common.CodeMessager {
|
||||
md.C.Request.URL.RawQuery = "id=self&" + md.C.Request.URL.RawQuery
|
||||
md.Ctx.Request.URI().SetQueryString("id=self")
|
||||
return UsersGET(md)
|
||||
}
|
||||
|
||||
func safeUsernameBulk(us [][]byte) [][]byte {
|
||||
for _, u := range us {
|
||||
for idx, v := range u {
|
||||
if v == ' ' {
|
||||
u[idx] = '_'
|
||||
continue
|
||||
}
|
||||
u[idx] = byte(unicode.ToLower(rune(v)))
|
||||
}
|
||||
}
|
||||
return us
|
||||
}
|
||||
|
||||
type whatIDResponse struct {
|
||||
common.ResponseBase
|
||||
ID int `json:"id"`
|
||||
@@ -110,16 +155,24 @@ type whatIDResponse struct {
|
||||
func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager {
|
||||
var (
|
||||
r whatIDResponse
|
||||
allowed int
|
||||
privileges uint64
|
||||
)
|
||||
err := md.DB.QueryRow("SELECT id, allowed FROM users WHERE username = ? LIMIT 1", md.C.Query("name")).Scan(&r.ID, &allowed)
|
||||
if err != nil || (allowed != 1 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) {
|
||||
err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username_safe = ? LIMIT 1", common.SafeUsername(md.Query("name"))).Scan(&r.ID, &privileges)
|
||||
if err != nil || ((privileges&uint64(common.UserPrivilegePublic)) == 0 &&
|
||||
(md.User.UserPrivileges&common.AdminPrivilegeManageUsers == 0)) {
|
||||
return common.SimpleResponse(404, "That user could not be found!")
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
var modesToReadable = [...]string{
|
||||
"std",
|
||||
"taiko",
|
||||
"ctb",
|
||||
"mania",
|
||||
}
|
||||
|
||||
type modeData struct {
|
||||
RankedScore uint64 `json:"ranked_score"`
|
||||
TotalScore uint64 `json:"total_score"`
|
||||
@@ -129,7 +182,8 @@ type modeData struct {
|
||||
Level float64 `json:"level"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP int `json:"pp"`
|
||||
GlobalLeaderboardRank int `json:"global_leaderboard_rank"`
|
||||
GlobalLeaderboardRank *int `json:"global_leaderboard_rank"`
|
||||
CountryLeaderboardRank *int `json:"country_leaderboard_rank"`
|
||||
}
|
||||
type userFullResponse struct {
|
||||
common.ResponseBase
|
||||
@@ -140,7 +194,13 @@ type userFullResponse struct {
|
||||
Mania modeData `json:"mania"`
|
||||
PlayStyle int `json:"play_style"`
|
||||
FavouriteMode int `json:"favourite_mode"`
|
||||
Badges []int `json:"badges"`
|
||||
Badges []singleBadge `json:"badges"`
|
||||
CustomBadge *singleBadge `json:"custom_badge"`
|
||||
SilenceInfo silenceInfo `json:"silence_info"`
|
||||
}
|
||||
type silenceInfo struct {
|
||||
Reason string `json:"reason"`
|
||||
End common.UnixTimestamp `json:"end"`
|
||||
}
|
||||
|
||||
// UserFullGET gets all of an user's information, with one exception: their userpage.
|
||||
@@ -153,71 +213,69 @@ func UserFullGET(md common.MethodData) common.CodeMessager {
|
||||
// Hellest query I've ever done.
|
||||
query := `
|
||||
SELECT
|
||||
users.id, users.username, users.register_datetime, users.rank, users.latest_activity,
|
||||
users.id, users.username, users.register_datetime, users.privileges, users.latest_activity,
|
||||
|
||||
users_stats.username_aka, users_stats.badges_shown, users_stats.country, users_stats.show_country,
|
||||
users_stats.play_style, users_stats.favourite_mode,
|
||||
users_stats.username_aka, users_stats.country, users_stats.play_style, users_stats.favourite_mode,
|
||||
|
||||
users_stats.custom_badge_icon, users_stats.custom_badge_name, users_stats.can_custom_badge,
|
||||
users_stats.show_custom_badge,
|
||||
|
||||
users_stats.ranked_score_std, users_stats.total_score_std, users_stats.playcount_std,
|
||||
users_stats.replays_watched_std, users_stats.total_hits_std,
|
||||
users_stats.avg_accuracy_std, users_stats.pp_std, leaderboard_std.position as std_position,
|
||||
users_stats.avg_accuracy_std, users_stats.pp_std,
|
||||
|
||||
users_stats.ranked_score_taiko, users_stats.total_score_taiko, users_stats.playcount_taiko,
|
||||
users_stats.replays_watched_taiko, users_stats.total_hits_taiko,
|
||||
users_stats.avg_accuracy_taiko, users_stats.pp_taiko, leaderboard_taiko.position as taiko_position,
|
||||
users_stats.avg_accuracy_taiko, users_stats.pp_taiko,
|
||||
|
||||
users_stats.ranked_score_ctb, users_stats.total_score_ctb, users_stats.playcount_ctb,
|
||||
users_stats.replays_watched_ctb, users_stats.total_hits_ctb,
|
||||
users_stats.avg_accuracy_ctb, users_stats.pp_ctb, leaderboard_ctb.position as ctb_position,
|
||||
users_stats.avg_accuracy_ctb, users_stats.pp_ctb,
|
||||
|
||||
users_stats.ranked_score_mania, users_stats.total_score_mania, users_stats.playcount_mania,
|
||||
users_stats.replays_watched_mania, users_stats.total_hits_mania,
|
||||
users_stats.avg_accuracy_mania, users_stats.pp_mania, leaderboard_mania.position as mania_position
|
||||
users_stats.avg_accuracy_mania, users_stats.pp_mania,
|
||||
|
||||
users.silence_reason, users.silence_end
|
||||
|
||||
FROM users
|
||||
LEFT JOIN users_stats
|
||||
ON users.id=users_stats.id
|
||||
LEFT JOIN leaderboard_std
|
||||
ON users.id=leaderboard_std.user
|
||||
LEFT JOIN leaderboard_taiko
|
||||
ON users.id=leaderboard_taiko.user
|
||||
LEFT JOIN leaderboard_ctb
|
||||
ON users.id=leaderboard_ctb.user
|
||||
LEFT JOIN leaderboard_mania
|
||||
ON users.id=leaderboard_mania.user
|
||||
WHERE ` + whereClause + ` AND users.allowed = '1'
|
||||
WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + `
|
||||
LIMIT 1
|
||||
`
|
||||
// Fuck.
|
||||
r := userFullResponse{}
|
||||
var (
|
||||
badges string
|
||||
country string
|
||||
showCountry bool
|
||||
registeredOn int64
|
||||
latestActivity int64
|
||||
b singleBadge
|
||||
can bool
|
||||
show bool
|
||||
)
|
||||
err := md.DB.QueryRow(query, param).Scan(
|
||||
&r.ID, &r.Username, ®isteredOn, &r.Rank, &latestActivity,
|
||||
&r.ID, &r.Username, &r.RegisteredOn, &r.Privileges, &r.LatestActivity,
|
||||
|
||||
&r.UsernameAKA, &badges, &country, &showCountry,
|
||||
&r.UsernameAKA, &r.Country,
|
||||
&r.PlayStyle, &r.FavouriteMode,
|
||||
|
||||
&b.Icon, &b.Name, &can, &show,
|
||||
|
||||
&r.STD.RankedScore, &r.STD.TotalScore, &r.STD.PlayCount,
|
||||
&r.STD.ReplaysWatched, &r.STD.TotalHits,
|
||||
&r.STD.Accuracy, &r.STD.PP, &r.STD.GlobalLeaderboardRank,
|
||||
&r.STD.Accuracy, &r.STD.PP,
|
||||
|
||||
&r.Taiko.RankedScore, &r.Taiko.TotalScore, &r.Taiko.PlayCount,
|
||||
&r.Taiko.ReplaysWatched, &r.Taiko.TotalHits,
|
||||
&r.Taiko.Accuracy, &r.Taiko.PP, &r.Taiko.GlobalLeaderboardRank,
|
||||
&r.Taiko.Accuracy, &r.Taiko.PP,
|
||||
|
||||
&r.CTB.RankedScore, &r.CTB.TotalScore, &r.CTB.PlayCount,
|
||||
&r.CTB.ReplaysWatched, &r.CTB.TotalHits,
|
||||
&r.CTB.Accuracy, &r.CTB.PP, &r.CTB.GlobalLeaderboardRank,
|
||||
&r.CTB.Accuracy, &r.CTB.PP,
|
||||
|
||||
&r.Mania.RankedScore, &r.Mania.TotalScore, &r.Mania.PlayCount,
|
||||
&r.Mania.ReplaysWatched, &r.Mania.TotalHits,
|
||||
&r.Mania.Accuracy, &r.Mania.PP, &r.Mania.GlobalLeaderboardRank,
|
||||
&r.Mania.Accuracy, &r.Mania.PP,
|
||||
|
||||
&r.SilenceInfo.Reason, &r.SilenceInfo.End,
|
||||
)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
@@ -227,14 +285,36 @@ LIMIT 1
|
||||
return Err500
|
||||
}
|
||||
|
||||
r.Country = genCountry(md, r.ID, showCountry, country)
|
||||
r.Badges = badgesToArray(badges)
|
||||
can = can && show && common.UserPrivileges(r.Privileges)&common.UserPrivilegeDonor > 0
|
||||
if can && (b.Name != "" || b.Icon != "") {
|
||||
r.CustomBadge = &b
|
||||
}
|
||||
|
||||
r.RegisteredOn = time.Unix(registeredOn, 0)
|
||||
r.LatestActivity = time.Unix(latestActivity, 0)
|
||||
|
||||
for _, m := range []*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
|
||||
for modeID, m := range [...]*modeData{&r.STD, &r.Taiko, &r.CTB, &r.Mania} {
|
||||
m.Level = ocl.GetLevelPrecise(int64(m.TotalScore))
|
||||
|
||||
if i := leaderboardPosition(md.R, modesToReadable[modeID], r.ID); i != nil {
|
||||
m.GlobalLeaderboardRank = i
|
||||
}
|
||||
if i := countryPosition(md.R, modesToReadable[modeID], r.ID, r.Country); i != nil {
|
||||
m.CountryLeaderboardRank = i
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT b.id, b.name, b.icon FROM user_badges ub "+
|
||||
"LEFT JOIN badges b ON ub.badge = b.id WHERE user = ?", r.ID)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var badge singleBadge
|
||||
err := rows.Scan(&badge.ID, &badge.Name, &badge.Icon)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
continue
|
||||
}
|
||||
r.Badges = append(r.Badges, badge)
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
@@ -243,7 +323,7 @@ LIMIT 1
|
||||
|
||||
type userpageResponse struct {
|
||||
common.ResponseBase
|
||||
Userpage string `json:"userpage"`
|
||||
Userpage *string `json:"userpage"`
|
||||
}
|
||||
|
||||
// UserUserpageGET gets an user's userpage, as in the customisable thing.
|
||||
@@ -261,23 +341,44 @@ func UserUserpageGET(md common.MethodData) common.CodeMessager {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if r.Userpage == nil {
|
||||
r.Userpage = new(string)
|
||||
}
|
||||
r.Code = 200
|
||||
return r
|
||||
}
|
||||
|
||||
// UserSelfUserpagePOST allows to change the current user's userpage.
|
||||
func UserSelfUserpagePOST(md common.MethodData) common.CodeMessager {
|
||||
var d struct {
|
||||
Data *string `json:"data"`
|
||||
}
|
||||
md.Unmarshal(&d)
|
||||
if d.Data == nil {
|
||||
return ErrMissingField("data")
|
||||
}
|
||||
cont := common.SanitiseString(*d.Data)
|
||||
_, err := md.DB.Exec("UPDATE users_stats SET userpage_content = ? WHERE id = ? LIMIT 1", cont, md.ID())
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
}
|
||||
md.Ctx.URI().SetQueryString("id=self")
|
||||
return UserUserpageGET(md)
|
||||
}
|
||||
|
||||
func whereClauseUser(md common.MethodData, tableName string) (*common.CodeMessager, string, interface{}) {
|
||||
switch {
|
||||
case md.C.Query("id") == "self":
|
||||
case md.Query("id") == "self":
|
||||
return nil, tableName + ".id = ?", md.ID()
|
||||
case md.C.Query("id") != "":
|
||||
id, err := strconv.Atoi(md.C.Query("id"))
|
||||
case md.Query("id") != "":
|
||||
id, err := strconv.Atoi(md.Query("id"))
|
||||
if err != nil {
|
||||
a := common.SimpleResponse(400, "please pass a valid user ID")
|
||||
return &a, "", nil
|
||||
}
|
||||
return nil, tableName + ".id = ?", id
|
||||
case md.C.Query("name") != "":
|
||||
return nil, tableName + ".username = ?", md.C.Query("name")
|
||||
case md.Query("name") != "":
|
||||
return nil, tableName + ".username_safe = ?", common.SafeUsername(md.Query("name"))
|
||||
}
|
||||
a := common.SimpleResponse(400, "you need to pass either querystring parameters name or id")
|
||||
return &a, "", nil
|
||||
@@ -295,23 +396,32 @@ type lookupUser struct {
|
||||
// UserLookupGET does a quick lookup of users beginning with the passed
|
||||
// querystring value name.
|
||||
func UserLookupGET(md common.MethodData) common.CodeMessager {
|
||||
name := strings.NewReplacer(
|
||||
name := common.SafeUsername(md.Query("name"))
|
||||
name = strings.NewReplacer(
|
||||
"%", "\\%",
|
||||
"_", "\\_",
|
||||
"\\", "\\\\",
|
||||
).Replace(md.C.Query("name"))
|
||||
).Replace(name)
|
||||
if name == "" {
|
||||
return common.SimpleResponse(400, "please provide an username to start searching")
|
||||
}
|
||||
name = "%" + name + "%"
|
||||
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE username LIKE ? AND allowed = '1' LIMIT 25", name)
|
||||
|
||||
var email string
|
||||
if md.User.TokenPrivileges&common.PrivilegeManageUser != 0 &&
|
||||
strings.Contains(md.Query("name"), "@") {
|
||||
email = md.Query("name")
|
||||
}
|
||||
|
||||
rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE "+
|
||||
"(username_safe LIKE ? OR email = ?) AND "+
|
||||
md.User.OnlyUserPublic(true)+" LIMIT 25", name, email)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
|
||||
var r userLookupResponse
|
||||
|
||||
for rows.Next() {
|
||||
var l lookupUser
|
||||
err := rows.Scan(&l.ID, &l.Username)
|
||||
|
@@ -2,35 +2,16 @@ package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
type score struct {
|
||||
ID int `json:"id"`
|
||||
BeatmapMD5 string `json:"beatmap_md5"`
|
||||
Score int64 `json:"score"`
|
||||
MaxCombo int `json:"max_combo"`
|
||||
FullCombo bool `json:"full_combo"`
|
||||
Mods int `json:"mods"`
|
||||
Count300 int `json:"count_300"`
|
||||
Count100 int `json:"count_100"`
|
||||
Count50 int `json:"count_50"`
|
||||
CountGeki int `json:"count_geki"`
|
||||
CountKatu int `json:"count_katu"`
|
||||
CountMiss int `json:"count_miss"`
|
||||
Time time.Time `json:"time"`
|
||||
PlayMode int `json:"play_mode"`
|
||||
Accuracy float64 `json:"accuracy"`
|
||||
PP float32 `json:"pp"`
|
||||
Completed int `json:"completed"`
|
||||
}
|
||||
|
||||
type userScore struct {
|
||||
score
|
||||
Beatmap *beatmap `json:"beatmap"`
|
||||
Score
|
||||
Beatmap beatmap `json:"beatmap"`
|
||||
}
|
||||
|
||||
type userScoresResponse struct {
|
||||
@@ -48,12 +29,13 @@ SELECT
|
||||
scores.completed,
|
||||
|
||||
beatmaps.beatmap_id, beatmaps.beatmapset_id, beatmaps.beatmap_md5,
|
||||
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty,
|
||||
beatmaps.song_name, beatmaps.ar, beatmaps.od, beatmaps.difficulty_std,
|
||||
beatmaps.difficulty_taiko, beatmaps.difficulty_ctb, beatmaps.difficulty_mania,
|
||||
beatmaps.max_combo, beatmaps.hit_length, beatmaps.ranked,
|
||||
beatmaps.ranked_status_freezed, beatmaps.latest_update
|
||||
FROM scores
|
||||
LEFT JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
LEFT JOIN users ON users.id = scores.userid
|
||||
INNER JOIN beatmaps ON beatmaps.beatmap_md5 = scores.beatmap_md5
|
||||
INNER JOIN users ON users.id = scores.userid
|
||||
`
|
||||
|
||||
// UserScoresBestGET retrieves the best scores of an user, sorted by PP if
|
||||
@@ -65,7 +47,7 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
|
||||
}
|
||||
mc := genModeClause(md)
|
||||
// Do not print 0pp scores on std
|
||||
if getMode(md.C.Query("mode")) == "std" {
|
||||
if getMode(md.Query("mode")) == "std" {
|
||||
mc += " AND scores.pp > 0"
|
||||
}
|
||||
return scoresPuts(md, fmt.Sprintf(
|
||||
@@ -73,9 +55,9 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager {
|
||||
scores.completed = '3'
|
||||
AND %s
|
||||
%s
|
||||
AND users.allowed = '1'
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.pp DESC, scores.score DESC %s`,
|
||||
wc, mc, common.Paginate(md.C.Query("p"), md.C.Query("l"), 100),
|
||||
wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
@@ -89,36 +71,12 @@ func UserScoresRecentGET(md common.MethodData) common.CodeMessager {
|
||||
`WHERE
|
||||
%s
|
||||
%s
|
||||
AND users.allowed = '1'
|
||||
ORDER BY scores.time DESC %s`,
|
||||
wc, genModeClause(md), common.Paginate(md.C.Query("p"), md.C.Query("l"), 100),
|
||||
AND `+md.User.OnlyUserPublic(true)+`
|
||||
ORDER BY scores.id DESC %s`,
|
||||
wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100),
|
||||
), param)
|
||||
}
|
||||
|
||||
func getMode(m string) string {
|
||||
switch m {
|
||||
case "1":
|
||||
return "taiko"
|
||||
case "2":
|
||||
return "ctb"
|
||||
case "3":
|
||||
return "mania"
|
||||
default:
|
||||
return "std"
|
||||
}
|
||||
}
|
||||
|
||||
func genModeClause(md common.MethodData) string {
|
||||
var modeClause string
|
||||
if md.C.Query("mode") != "" {
|
||||
m, err := strconv.Atoi(md.C.Query("mode"))
|
||||
if err == nil && m >= 0 && m <= 3 {
|
||||
modeClause = fmt.Sprintf("AND scores.play_mode = '%d'", m)
|
||||
}
|
||||
}
|
||||
return modeClause
|
||||
}
|
||||
|
||||
func scoresPuts(md common.MethodData, whereClause string, params ...interface{}) common.CodeMessager {
|
||||
rows, err := md.DB.Query(userScoreSelectBase+whereClause, params...)
|
||||
if err != nil {
|
||||
@@ -129,39 +87,37 @@ func scoresPuts(md common.MethodData, whereClause string, params ...interface{})
|
||||
for rows.Next() {
|
||||
var (
|
||||
us userScore
|
||||
t string
|
||||
b beatmapMayOrMayNotExist
|
||||
rawLatestUpdate *int64
|
||||
b beatmap
|
||||
)
|
||||
err = rows.Scan(
|
||||
&us.ID, &us.BeatmapMD5, &us.Score,
|
||||
&us.ID, &us.BeatmapMD5, &us.Score.Score,
|
||||
&us.MaxCombo, &us.FullCombo, &us.Mods,
|
||||
&us.Count300, &us.Count100, &us.Count50,
|
||||
&us.CountGeki, &us.CountKatu, &us.CountMiss,
|
||||
&t, &us.PlayMode, &us.Accuracy, &us.PP,
|
||||
&us.Time, &us.PlayMode, &us.Accuracy, &us.PP,
|
||||
&us.Completed,
|
||||
|
||||
&b.BeatmapID, &b.BeatmapsetID, &b.BeatmapMD5,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Difficulty,
|
||||
&b.SongName, &b.AR, &b.OD, &b.Diff2.STD,
|
||||
&b.Diff2.Taiko, &b.Diff2.CTB, &b.Diff2.Mania,
|
||||
&b.MaxCombo, &b.HitLength, &b.Ranked,
|
||||
&b.RankedStatusFrozen, &rawLatestUpdate,
|
||||
&b.RankedStatusFrozen, &b.LatestUpdate,
|
||||
)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
// puck feppy
|
||||
us.Time, err = time.Parse(common.OsuTimeFormat, t)
|
||||
if err != nil {
|
||||
md.Err(err)
|
||||
return Err500
|
||||
}
|
||||
if rawLatestUpdate != nil {
|
||||
// fml i should have used an inner join
|
||||
xd := time.Unix(*rawLatestUpdate, 0)
|
||||
b.LatestUpdate = &xd
|
||||
}
|
||||
us.Beatmap = b.toBeatmap()
|
||||
b.Difficulty = b.Diff2.STD
|
||||
us.Beatmap = b
|
||||
us.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(us.PlayMode),
|
||||
osuapi.Mods(us.Mods),
|
||||
us.Accuracy,
|
||||
us.Count300,
|
||||
us.Count100,
|
||||
us.Count50,
|
||||
us.CountMiss,
|
||||
))
|
||||
scores = append(scores, us)
|
||||
}
|
||||
r := userScoresResponse{}
|
||||
|
18
app/websockets/entry.go
Normal file
18
app/websockets/entry.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/leavengood/websocket"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var upgrader = websocket.FastHTTPUpgrader{
|
||||
Handler: handler,
|
||||
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebsocketV1Entry upgrades a connection to a websocket.
|
||||
func WebsocketV1Entry(ctx *fasthttp.RequestCtx) {
|
||||
upgrader.UpgradeHandler(ctx)
|
||||
}
|
100
app/websockets/main_handler.go
Normal file
100
app/websockets/main_handler.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/leavengood/websocket"
|
||||
)
|
||||
|
||||
var stepNumber uint64
|
||||
|
||||
func handler(rawConn *websocket.Conn) {
|
||||
defer catchPanic()
|
||||
defer rawConn.Close()
|
||||
|
||||
step := atomic.AddUint64(&stepNumber, 1)
|
||||
|
||||
// 5 is a security margin in case
|
||||
if step == (1<<10 - 5) {
|
||||
atomic.StoreUint64(&stepNumber, 0)
|
||||
}
|
||||
|
||||
c := &conn{
|
||||
rawConn,
|
||||
sync.Mutex{},
|
||||
step | uint64(time.Now().UnixNano()<<10),
|
||||
}
|
||||
|
||||
c.WriteJSON(TypeConnected, nil)
|
||||
|
||||
defer cleanup(c.ID)
|
||||
|
||||
for {
|
||||
var i incomingMessage
|
||||
err := c.Conn.ReadJSON(&i)
|
||||
if _, ok := err.(*websocket.CloseError); ok {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
continue
|
||||
}
|
||||
f, ok := messageHandler[i.Type]
|
||||
if !ok {
|
||||
c.WriteJSON(TypeInvalidMessage, "invalid message type")
|
||||
continue
|
||||
}
|
||||
f(c, i)
|
||||
}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
Conn *websocket.Conn
|
||||
Mtx sync.Mutex
|
||||
ID uint64
|
||||
}
|
||||
|
||||
func (c *conn) WriteJSON(t string, data interface{}) error {
|
||||
c.Mtx.Lock()
|
||||
err := c.Conn.WriteJSON(newMessage(t, data))
|
||||
c.Mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
var messageHandler = map[string]func(c *conn, message incomingMessage){
|
||||
TypeSubscribeScores: SubscribeScores,
|
||||
}
|
||||
|
||||
// Server Message Types
|
||||
const (
|
||||
TypeConnected = "connected"
|
||||
TypeInvalidMessage = "invalid_message_type"
|
||||
TypeSubscribedToScores = "subscribed_to_scores"
|
||||
TypeNewScore = "new_score"
|
||||
)
|
||||
|
||||
// Client Message Types
|
||||
const (
|
||||
TypeSubscribeScores = "subscribe_scores"
|
||||
)
|
||||
|
||||
// Message is the wrapped information for a message sent to the client.
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func newMessage(t string, data interface{}) Message {
|
||||
return Message{
|
||||
Type: t,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
type incomingMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
142
app/websockets/scores.go
Normal file
142
app/websockets/scores.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/x/getrank"
|
||||
)
|
||||
|
||||
type subscribeScoresUser struct {
|
||||
User int `json:"user"`
|
||||
Modes []int `json:"modes"`
|
||||
}
|
||||
|
||||
// SubscribeScores subscribes a connection to score updates.
|
||||
func SubscribeScores(c *conn, message incomingMessage) {
|
||||
var ssu []subscribeScoresUser
|
||||
err := json.Unmarshal(message.Data, &ssu)
|
||||
if err != nil {
|
||||
c.WriteJSON(TypeInvalidMessage, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
|
||||
var found bool
|
||||
for idx, el := range scoreSubscriptions {
|
||||
// already exists, change the users
|
||||
if el.Conn.ID == c.ID {
|
||||
found = true
|
||||
scoreSubscriptions[idx].Users = ssu
|
||||
}
|
||||
}
|
||||
|
||||
// if it was not found, we need to add it
|
||||
if !found {
|
||||
scoreSubscriptions = append(scoreSubscriptions, scoreSubscription{c, ssu})
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
|
||||
c.WriteJSON(TypeSubscribedToScores, ssu)
|
||||
}
|
||||
|
||||
type scoreSubscription struct {
|
||||
Conn *conn
|
||||
Users []subscribeScoresUser
|
||||
}
|
||||
|
||||
var scoreSubscriptions []scoreSubscription
|
||||
var scoreSubscriptionsMtx = new(sync.RWMutex)
|
||||
|
||||
func scoreRetriever() {
|
||||
ps, err := red.Subscribe("api:score_submission")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
for {
|
||||
msg, err := ps.ReceiveMessage()
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
go handleNewScore(msg.Payload)
|
||||
}
|
||||
}
|
||||
|
||||
type score struct {
|
||||
v1.Score
|
||||
UserID int `json:"user_id"`
|
||||
}
|
||||
|
||||
func handleNewScore(id string) {
|
||||
defer catchPanic()
|
||||
var s score
|
||||
err := db.Get(&s, `
|
||||
SELECT
|
||||
id, beatmap_md5, score, max_combo, full_combo, mods,
|
||||
300_count, 100_count, 50_count, gekis_count, katus_count, misses_count,
|
||||
time, play_mode, accuracy, pp, completed, userid AS user_id
|
||||
FROM scores WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
s.Rank = strings.ToUpper(getrank.GetRank(
|
||||
osuapi.Mode(s.PlayMode),
|
||||
osuapi.Mods(s.Mods),
|
||||
s.Accuracy,
|
||||
s.Count300,
|
||||
s.Count100,
|
||||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
scoreSubscriptionsMtx.RLock()
|
||||
cp := make([]scoreSubscription, len(scoreSubscriptions))
|
||||
copy(cp, scoreSubscriptions)
|
||||
scoreSubscriptionsMtx.RUnlock()
|
||||
|
||||
for _, el := range cp {
|
||||
if len(el.Users) > 0 && !scoreUserValid(el.Users, s) {
|
||||
continue
|
||||
}
|
||||
|
||||
el.Conn.WriteJSON(TypeNewScore, s)
|
||||
}
|
||||
}
|
||||
|
||||
func scoreUserValid(users []subscribeScoresUser, s score) bool {
|
||||
for _, u := range users {
|
||||
if u.User == s.UserID {
|
||||
if len(u.Modes) > 0 {
|
||||
if !inModes(u.Modes, s.PlayMode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inModes(modes []int, i int) bool {
|
||||
for _, m := range modes {
|
||||
if m == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func catchPanic() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
fmt.Println(r)
|
||||
// TODO: sentry
|
||||
}
|
||||
}
|
32
app/websockets/websockets.go
Normal file
32
app/websockets/websockets.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Package websockets implements functionality related to the API websockets.
|
||||
package websockets
|
||||
|
||||
import (
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
var (
|
||||
red *redis.Client
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
// Start begins websocket functionality
|
||||
func Start(r *redis.Client, _db *sqlx.DB) error {
|
||||
red = r
|
||||
db = _db
|
||||
go scoreRetriever()
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup(connID uint64) {
|
||||
scoreSubscriptionsMtx.Lock()
|
||||
for idx, el := range scoreSubscriptions {
|
||||
if el.Conn.ID == connID {
|
||||
scoreSubscriptions[idx] = scoreSubscriptions[len(scoreSubscriptions)-1]
|
||||
scoreSubscriptions = scoreSubscriptions[:len(scoreSubscriptions)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
scoreSubscriptionsMtx.Unlock()
|
||||
}
|
185
beatmapget/beatmapget.go
Normal file
185
beatmapget/beatmapget.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Package beatmapget is an helper package to retrieve beatmap information from
|
||||
// the osu! API, if the beatmap in the database is too old.
|
||||
package beatmapget
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Expire is the duration after which a beatmap expires.
|
||||
const Expire = time.Hour * 12
|
||||
|
||||
// DB is the database.
|
||||
var DB *sqlx.DB
|
||||
|
||||
// Client is the osu! api client to use
|
||||
var Client *osuapi.Client
|
||||
|
||||
// BeatmapDefiningQuality is the defining quality of the beatmap to be updated,
|
||||
// which is to say either the ID, the set ID or the md5 hash.
|
||||
type BeatmapDefiningQuality struct {
|
||||
ID int
|
||||
MD5 string
|
||||
frozen bool
|
||||
ranked int
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) String() string {
|
||||
if b.MD5 != "" {
|
||||
return "#/" + b.MD5
|
||||
}
|
||||
if b.ID != 0 {
|
||||
return "b/" + strconv.Itoa(b.ID)
|
||||
}
|
||||
return "n/a"
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) isSomethingSet() error {
|
||||
if b.ID == 0 && b.MD5 == "" {
|
||||
return errors.New("beatmapget: at least one field in BeatmapDefiningQuality must be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b BeatmapDefiningQuality) whereAndParams() (where string, params []interface{}) {
|
||||
var wheres []string
|
||||
if b.ID != 0 {
|
||||
wheres = append(wheres, "beatmap_id = ?")
|
||||
params = append(params, b.ID)
|
||||
}
|
||||
if b.MD5 != "" {
|
||||
wheres = append(wheres, "beatmap_md5 = ?")
|
||||
params = append(params, b.MD5)
|
||||
}
|
||||
where = strings.Join(wheres, " AND ")
|
||||
if where == "" {
|
||||
where = "1"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateIfRequired updates the beatmap in the database if an update is required.
|
||||
func UpdateIfRequired(b BeatmapDefiningQuality) error {
|
||||
required, err := UpdateRequired(&b)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
if !required {
|
||||
return nil
|
||||
}
|
||||
return Update(b, err != sql.ErrNoRows)
|
||||
}
|
||||
|
||||
// UpdateRequired checks an update is required. If error is sql.ErrNoRows,
|
||||
// it means that the beatmap should be updated, and that there was not the
|
||||
// beatmap in the database previously.
|
||||
func UpdateRequired(b *BeatmapDefiningQuality) (bool, error) {
|
||||
if err := b.isSomethingSet(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
where, params := b.whereAndParams()
|
||||
var data struct {
|
||||
Difficulties [3]float64
|
||||
Ranked int
|
||||
Frozen bool
|
||||
LatestUpdate common.UnixTimestamp
|
||||
}
|
||||
err := DB.QueryRow("SELECT difficulty_taiko, difficulty_ctb, difficulty_mania, ranked,"+
|
||||
"ranked_status_freezed, latest_update FROM beatmaps WHERE "+
|
||||
where+" LIMIT 1", params...).
|
||||
Scan(
|
||||
&data.Difficulties[0], &data.Difficulties[1], &data.Difficulties[2],
|
||||
&data.Ranked, &data.Frozen, &data.LatestUpdate,
|
||||
)
|
||||
b.frozen = data.Frozen
|
||||
if b.frozen {
|
||||
b.ranked = data.Ranked
|
||||
}
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
if data.Difficulties[0] == 0 && data.Difficulties[1] == 0 && data.Difficulties[2] == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
expire := Expire
|
||||
if data.Ranked == 2 {
|
||||
expire *= 6
|
||||
}
|
||||
|
||||
if expire != 0 && time.Now().After(time.Time(data.LatestUpdate).Add(expire)) && !data.Frozen {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Update updates a beatmap.
|
||||
func Update(b BeatmapDefiningQuality, beatmapInDB bool) error {
|
||||
var data [4]osuapi.Beatmap
|
||||
for i := 0; i <= 3; i++ {
|
||||
mode := osuapi.Mode(i)
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapID: b.ID,
|
||||
BeatmapHash: b.MD5,
|
||||
Mode: &mode,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(beatmaps) == 0 {
|
||||
continue
|
||||
}
|
||||
data[i] = beatmaps[0]
|
||||
}
|
||||
var main *osuapi.Beatmap
|
||||
for _, el := range data {
|
||||
if el.FileMD5 != "" {
|
||||
main = &el
|
||||
break
|
||||
}
|
||||
}
|
||||
if main == nil {
|
||||
return fmt.Errorf("beatmapget: beatmap %s not found", b.String())
|
||||
}
|
||||
if beatmapInDB {
|
||||
w, p := b.whereAndParams()
|
||||
DB.MustExec("DELETE FROM beatmaps WHERE "+w, p...)
|
||||
}
|
||||
if b.frozen {
|
||||
main.Approved = osuapi.ApprovedStatus(b.ranked)
|
||||
}
|
||||
songName := fmt.Sprintf("%s - %s [%s]", main.Artist, main.Title, main.DiffName)
|
||||
_, err := DB.Exec(`INSERT INTO
|
||||
beatmaps (
|
||||
beatmap_id, beatmapset_id, beatmap_md5,
|
||||
song_name, ar, od, difficulty_std, difficulty_taiko,
|
||||
difficulty_ctb, difficulty_mania, max_combo, hit_length,
|
||||
bpm, ranked, latest_update, ranked_status_freezed
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
|
||||
main.BeatmapID, main.BeatmapSetID, main.FileMD5,
|
||||
songName, main.ApproachRate, main.OverallDifficulty, data[0].DifficultyRating, data[1].DifficultyRating,
|
||||
data[2].DifficultyRating, data[3].DifficultyRating, main.MaxCombo, main.HitLength,
|
||||
int(main.BPM), main.Approved, time.Now().Unix(), b.frozen,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
osuapi.RateLimit(200)
|
||||
}
|
81
beatmapget/fullset.go
Normal file
81
beatmapget/fullset.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package beatmapget
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Set checks if an update is required for all beatmaps in a set.
|
||||
func Set(s int) error {
|
||||
var (
|
||||
lastUpdated common.UnixTimestamp
|
||||
ranked int
|
||||
)
|
||||
err := DB.QueryRow("SELECT latest_update, ranked FROM beatmaps WHERE beatmapset_id = ? LIMIT 1", s).
|
||||
Scan(&lastUpdated, &ranked)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
return set(s, lastUpdated, ranked)
|
||||
}
|
||||
|
||||
// ErrBeatmapNotFound is returned by Beatmap if a beatmap could not be found.
|
||||
var ErrBeatmapNotFound = errors.New("beatmapget: beatmap not found")
|
||||
|
||||
// Beatmap check if an update is required for all beatmaps in the set
|
||||
// containing this beatmap.
|
||||
func Beatmap(b int) (int, error) {
|
||||
var (
|
||||
setID int
|
||||
lastUpdated common.UnixTimestamp
|
||||
ranked int
|
||||
)
|
||||
err := DB.QueryRow("SELECT beatmapset_id, latest_update, ranked FROM beatmaps WHERE beatmap_id = ? LIMIT 1", b).
|
||||
Scan(&setID, &lastUpdated, &ranked)
|
||||
switch err {
|
||||
case nil:
|
||||
return setID, set(setID, lastUpdated, ranked)
|
||||
case sql.ErrNoRows:
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapID: b,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(beatmaps) == 0 {
|
||||
return 0, ErrBeatmapNotFound
|
||||
}
|
||||
return beatmaps[0].BeatmapSetID, set(beatmaps[0].BeatmapSetID, common.UnixTimestamp(time.Time{}), 0)
|
||||
default:
|
||||
return setID, err
|
||||
}
|
||||
}
|
||||
|
||||
func set(s int, updated common.UnixTimestamp, ranked int) error {
|
||||
expire := Expire
|
||||
if ranked == 2 {
|
||||
expire *= 6
|
||||
}
|
||||
if time.Now().Before(time.Time(updated).Add(expire)) {
|
||||
return nil
|
||||
}
|
||||
beatmaps, err := Client.GetBeatmaps(osuapi.GetBeatmapsOpts{
|
||||
BeatmapSetID: s,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, beatmap := range beatmaps {
|
||||
err := UpdateIfRequired(BeatmapDefiningQuality{
|
||||
ID: beatmap.BeatmapID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -6,6 +6,10 @@ import (
|
||||
"github.com/thehowl/conf"
|
||||
)
|
||||
|
||||
// Version is the git hash of the application. Do not edit. This is
|
||||
// automatically set using -ldflags during build time.
|
||||
var Version string
|
||||
|
||||
// Conf is the configuration file data for the ripple API.
|
||||
// Conf uses https://github.com/thehowl/conf
|
||||
type Conf struct {
|
||||
@@ -13,6 +17,14 @@ type Conf struct {
|
||||
DSN string `description:"The Data Source Name for the database. More: https://github.com/go-sql-driver/mysql#dsn-data-source-name"`
|
||||
ListenTo string `description:"The IP/Port combination from which to take connections, e.g. :8080"`
|
||||
Unix bool `description:"Bool indicating whether ListenTo is a UNIX socket or an address."`
|
||||
SentryDSN string `description:"thing for sentry whatever"`
|
||||
HanayoKey string
|
||||
BeatmapRequestsPerUser int
|
||||
RankQueueSize int
|
||||
OsuAPIKey string
|
||||
RedisAddr string
|
||||
RedisPassword string
|
||||
RedisDB int
|
||||
}
|
||||
|
||||
var cachedConf *Conf
|
||||
@@ -31,9 +43,23 @@ func Load() (c Conf, halt bool) {
|
||||
DSN: "root@/ripple",
|
||||
ListenTo: ":40001",
|
||||
Unix: false,
|
||||
HanayoKey: "Potato",
|
||||
BeatmapRequestsPerUser: 2,
|
||||
RankQueueSize: 25,
|
||||
RedisAddr: "localhost:6379",
|
||||
}, "api.conf")
|
||||
fmt.Println("Please compile the configuration file (api.conf).")
|
||||
}
|
||||
cachedConf = &c
|
||||
return
|
||||
}
|
||||
|
||||
// GetConf returns the cachedConf.
|
||||
func GetConf() *Conf {
|
||||
if cachedConf == nil {
|
||||
return nil
|
||||
}
|
||||
// so that the cachedConf cannot actually get modified
|
||||
c := *cachedConf
|
||||
return &c
|
||||
}
|
||||
|
29
common/conversions.go
Normal file
29
common/conversions.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func s2b(s string) []byte {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
}
|
8
common/flags.go
Normal file
8
common/flags.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package common
|
||||
|
||||
// These are the flags an user can have. Mostly settings or things like whether
|
||||
// the user has verified their email address.
|
||||
const (
|
||||
FlagEmailVerified = 1 << iota
|
||||
FlagCountry2FA
|
||||
)
|
@@ -1,23 +1,126 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/DataDog/datadog-go/statsd"
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/valyala/fasthttp"
|
||||
"gopkg.in/redis.v5"
|
||||
)
|
||||
|
||||
// RavenClient is the raven client to which report errors happening.
|
||||
// If nil, errors will just be fmt.Println'd
|
||||
var RavenClient *raven.Client
|
||||
|
||||
// MethodData is a struct containing the data passed over to an API method.
|
||||
type MethodData struct {
|
||||
User Token
|
||||
DB *sql.DB
|
||||
RequestData RequestData
|
||||
C *gin.Context
|
||||
DB *sqlx.DB
|
||||
Doggo *statsd.Client
|
||||
R *redis.Client
|
||||
Ctx *fasthttp.RequestCtx
|
||||
}
|
||||
|
||||
// Err logs an error into gin.
|
||||
// ClientIP implements a best effort algorithm to return the real client IP, it parses
|
||||
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
|
||||
func (md MethodData) ClientIP() string {
|
||||
clientIP := strings.TrimSpace(string(md.Ctx.Request.Header.Peek("X-Real-Ip")))
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
clientIP = string(md.Ctx.Request.Header.Peek("X-Forwarded-For"))
|
||||
if index := strings.IndexByte(clientIP, ','); index >= 0 {
|
||||
clientIP = clientIP[0:index]
|
||||
}
|
||||
clientIP = strings.TrimSpace(clientIP)
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
return md.Ctx.RemoteIP().String()
|
||||
}
|
||||
|
||||
// Err logs an error. If RavenClient is set, it will use the client to report
|
||||
// the error to sentry, otherwise it will just write the error to stdout.
|
||||
func (md MethodData) Err(err error) {
|
||||
md.C.Error(err)
|
||||
user := &raven.User{
|
||||
ID: strconv.Itoa(md.User.UserID),
|
||||
Username: md.User.Value,
|
||||
IP: md.Ctx.RemoteAddr().String(),
|
||||
}
|
||||
// Generate tags for error
|
||||
tags := map[string]string{
|
||||
"endpoint": string(md.Ctx.RequestURI()),
|
||||
"token": md.User.Value,
|
||||
}
|
||||
_err(err, tags, user, md.Ctx)
|
||||
}
|
||||
|
||||
// Err for peppy API calls
|
||||
func Err(c *fasthttp.RequestCtx, err error) {
|
||||
// Generate tags for error
|
||||
tags := map[string]string{
|
||||
"endpoint": string(c.RequestURI()),
|
||||
}
|
||||
|
||||
_err(err, tags, nil, c)
|
||||
}
|
||||
|
||||
func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) {
|
||||
if RavenClient == nil {
|
||||
fmt.Println("ERROR!!!!")
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create stacktrace
|
||||
st := raven.NewStacktrace(0, 3, []string{"zxq.co/ripple", "git.zxq.co/ripple"})
|
||||
|
||||
ifaces := []raven.Interface{st, generateRavenHTTP(c)}
|
||||
if user != nil {
|
||||
ifaces = append(ifaces, user)
|
||||
}
|
||||
|
||||
RavenClient.CaptureError(
|
||||
err,
|
||||
tags,
|
||||
ifaces...,
|
||||
)
|
||||
}
|
||||
|
||||
func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
|
||||
// build uri
|
||||
uri := ctx.URI()
|
||||
// safe to use b2s because a new string gets allocated eventually for
|
||||
// concatenation
|
||||
sURI := b2s(uri.Scheme()) + "://" + b2s(uri.Host()) + b2s(uri.Path())
|
||||
|
||||
// build header map
|
||||
// using ctx.Request.Header.Len would mean calling .VisitAll two times
|
||||
// which can be quite expensive since it means iterating over all the
|
||||
// headers, so we give a rough estimate of the number of headers we expect
|
||||
// to have
|
||||
m := make(map[string]string, 16)
|
||||
ctx.Request.Header.VisitAll(func(k, v []byte) {
|
||||
// not using b2s because we mustn't keep references to the underlying
|
||||
// k and v
|
||||
m[string(k)] = string(v)
|
||||
})
|
||||
|
||||
return &raven.Http{
|
||||
URL: sURI,
|
||||
// Not using b2s because raven sending is concurrent and may happen
|
||||
// AFTER the request, meaning that values could potentially be replaced
|
||||
// by new ones.
|
||||
Method: string(ctx.Method()),
|
||||
Query: string(uri.QueryString()),
|
||||
Cookies: string(ctx.Request.Header.Peek("Cookie")),
|
||||
Headers: m,
|
||||
}
|
||||
}
|
||||
|
||||
// ID retrieves the Token's owner user ID.
|
||||
@@ -25,13 +128,23 @@ func (md MethodData) ID() int {
|
||||
return md.User.UserID
|
||||
}
|
||||
|
||||
// RequestData is the body of a request. It is wrapped into this type
|
||||
// to implement the Unmarshal function, which is just a shorthand to
|
||||
// json.Unmarshal.
|
||||
type RequestData []byte
|
||||
|
||||
// Unmarshal json-decodes Requestdata into a value. Basically a
|
||||
// shorthand to json.Unmarshal.
|
||||
func (r RequestData) Unmarshal(into interface{}) error {
|
||||
return json.Unmarshal([]byte(r), into)
|
||||
// Query is shorthand for md.C.Query.
|
||||
func (md MethodData) Query(q string) string {
|
||||
return b2s(md.Ctx.QueryArgs().Peek(q))
|
||||
}
|
||||
|
||||
// HasQuery returns true if the parameter is encountered in the querystring.
|
||||
// It returns true even if the parameter is "" (the case of ?param&etc=etc)
|
||||
func (md MethodData) HasQuery(q string) bool {
|
||||
return md.Ctx.QueryArgs().Has(q)
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals a request's JSON body into an interface.
|
||||
func (md MethodData) Unmarshal(into interface{}) error {
|
||||
return json.Unmarshal(md.Ctx.PostBody(), into)
|
||||
}
|
||||
|
||||
// IsBearer tells whether the current token is a Bearer (oauth) token.
|
||||
func (md MethodData) IsBearer() bool {
|
||||
return md.User.ID == -1
|
||||
}
|
||||
|
@@ -1,4 +0,0 @@
|
||||
package common
|
||||
|
||||
// OsuTimeFormat is the time format for scores in the DB. Can be used with time.Parse etc.
|
||||
const OsuTimeFormat = "060102150405"
|
@@ -1,42 +1,22 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// Paginate creates an additional SQL LIMIT clause for paginating.
|
||||
func Paginate(page, limit string, maxLimit int) string {
|
||||
var (
|
||||
pInt int
|
||||
lInt int
|
||||
err error
|
||||
p = Int(page)
|
||||
l = Int(limit)
|
||||
)
|
||||
if page == "" {
|
||||
pInt = 1
|
||||
} else {
|
||||
pInt, err = strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pInt = 1
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
if l < 1 {
|
||||
l = 50
|
||||
}
|
||||
if limit == "" {
|
||||
lInt = 50
|
||||
} else {
|
||||
lInt, err = strconv.Atoi(limit)
|
||||
if err != nil {
|
||||
lInt = 50
|
||||
if l > maxLimit {
|
||||
l = maxLimit
|
||||
}
|
||||
}
|
||||
if pInt < 1 {
|
||||
pInt = 1
|
||||
}
|
||||
if lInt < 1 {
|
||||
lInt = 50
|
||||
}
|
||||
if lInt > maxLimit {
|
||||
lInt = maxLimit
|
||||
}
|
||||
start := (pInt - 1) * lInt
|
||||
return fmt.Sprintf(" LIMIT %d,%d ", start, lInt)
|
||||
start := uint(p-1) * uint(l)
|
||||
return fmt.Sprintf(" LIMIT %d,%d ", start, l)
|
||||
}
|
||||
|
49
common/paginate_test.go
Normal file
49
common/paginate_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPaginate(t *testing.T) {
|
||||
type args struct {
|
||||
page string
|
||||
limit string
|
||||
maxLimit int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"1",
|
||||
args{
|
||||
"10",
|
||||
"",
|
||||
100,
|
||||
},
|
||||
" LIMIT 450,50 ",
|
||||
},
|
||||
{
|
||||
"2",
|
||||
args{
|
||||
"-5",
|
||||
"-15",
|
||||
100,
|
||||
},
|
||||
" LIMIT 0,50 ",
|
||||
},
|
||||
{
|
||||
"3",
|
||||
args{
|
||||
"2",
|
||||
"150",
|
||||
100,
|
||||
},
|
||||
" LIMIT 100,100 ",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := Paginate(tt.args.page, tt.args.limit, tt.args.maxLimit); got != tt.want {
|
||||
t.Errorf("%q. Paginate() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ import "strings"
|
||||
|
||||
// These are the various privileges a token can have.
|
||||
const (
|
||||
PrivilegeRead = 1 << iota // pretty much public data: leaderboard, scores, user profiles (without confidential stuff like email)
|
||||
PrivilegeRead = 1 << iota // used to be to fetch public data, such as user information etc. this is deprecated.
|
||||
PrivilegeReadConfidential // (eventual) private messages, reports... of self
|
||||
PrivilegeWrite // change user information, write into confidential stuff...
|
||||
PrivilegeManageBadges // can change various users' badges.
|
||||
@@ -22,71 +22,6 @@ const (
|
||||
// Privileges is a bitwise enum of the privileges of an user's API key.
|
||||
type Privileges uint64
|
||||
|
||||
// HasPrivilegeRead returns whether the Read privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeRead() bool {
|
||||
return p&PrivilegeRead != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeReadConfidential returns whether the ReadConfidential privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeReadConfidential() bool {
|
||||
return p&PrivilegeReadConfidential != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeWrite returns whether the Write privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeWrite() bool {
|
||||
return p&PrivilegeWrite != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeManageBadges returns whether the ManageBadges privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeManageBadges() bool {
|
||||
return p&PrivilegeManageBadges != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeBetaKeys returns whether the BetaKeys privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeBetaKeys() bool {
|
||||
return p&PrivilegeBetaKeys != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeManageSettings returns whether the ManageSettings privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeManageSettings() bool {
|
||||
return p&PrivilegeManageSettings != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeViewUserAdvanced returns whether the ViewUserAdvanced privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeViewUserAdvanced() bool {
|
||||
return p&PrivilegeViewUserAdvanced != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeManageUser returns whether the ManageUser privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeManageUser() bool {
|
||||
return p&PrivilegeManageUser != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeManageRoles returns whether the ManageRoles privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeManageRoles() bool {
|
||||
return p&PrivilegeManageRoles != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeManageAPIKeys returns whether the ManageAPIKeys privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeManageAPIKeys() bool {
|
||||
return p&PrivilegeManageAPIKeys != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeBlog returns whether the Blog privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeBlog() bool {
|
||||
return p&PrivilegeBlog != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeAPIMeta returns whether the APIMeta privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeAPIMeta() bool {
|
||||
return p&PrivilegeAPIMeta != 0
|
||||
}
|
||||
|
||||
// HasPrivilegeBeatmap returns whether the Beatmap privilege is included in the privileges.
|
||||
func (p Privileges) HasPrivilegeBeatmap() bool {
|
||||
return p&PrivilegeBeatmap != 0
|
||||
}
|
||||
|
||||
var privilegeString = [...]string{
|
||||
"Read",
|
||||
"ReadConfidential",
|
||||
@@ -106,35 +41,35 @@ var privilegeString = [...]string{
|
||||
func (p Privileges) String() string {
|
||||
var pvs []string
|
||||
for i, v := range privilegeString {
|
||||
if int(p)&(1<<uint(i)) != 0 {
|
||||
if uint64(p)&uint64(1<<uint(i)) != 0 {
|
||||
pvs = append(pvs, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(pvs, ", ")
|
||||
}
|
||||
|
||||
var privilegeMustBe = [...]int{
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
var privilegeMustBe = [...]UserPrivileges{
|
||||
1 << 30, // read is deprecated, and should be given out to no-one.
|
||||
UserPrivilegeNormal,
|
||||
UserPrivilegeNormal,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBadges,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBetaKey,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageSetting,
|
||||
AdminPrivilegeAccessRAP,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeBanUsers,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManagePrivilege,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageUsers | AdminPrivilegeManageServer,
|
||||
AdminPrivilegeChatMod, // temporary?
|
||||
AdminPrivilegeManageServer,
|
||||
AdminPrivilegeAccessRAP | AdminPrivilegeManageBeatmap,
|
||||
}
|
||||
|
||||
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
|
||||
func (p Privileges) CanOnly(rank int) Privileges {
|
||||
func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
|
||||
newPrivilege := 0
|
||||
for i, v := range privilegeMustBe {
|
||||
wants := p&1 == 1
|
||||
can := rank >= v
|
||||
can := userPrivs&v == v
|
||||
if wants && can {
|
||||
newPrivilege |= 1 << uint(i)
|
||||
}
|
||||
|
16
common/sanitisation.go
Normal file
16
common/sanitisation.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// SanitiseString removes all control codes from a string.
|
||||
func SanitiseString(s string) string {
|
||||
n := make([]rune, 0, len(s))
|
||||
for _, c := range s {
|
||||
if c == '\n' || !unicode.Is(unicode.Other, c) {
|
||||
n = append(n, c)
|
||||
}
|
||||
}
|
||||
return string(n)
|
||||
}
|
41
common/sanitisation_test.go
Normal file
41
common/sanitisation_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
const pen = "I trattori di palmizio 나는 펜이있다. 私はリンゴを持っています。" +
|
||||
"啊! 苹果笔。 у меня есть ручка, Tôi có dứa. අන්නාසි පෑන"
|
||||
|
||||
func TestSanitiseString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"Normal",
|
||||
pen,
|
||||
pen,
|
||||
},
|
||||
{
|
||||
"Arabic (rtl)",
|
||||
"أناناس",
|
||||
"أناناس",
|
||||
},
|
||||
{
|
||||
"Null",
|
||||
"A\x00B",
|
||||
"AB",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := SanitiseString(tt.arg); got != tt.want {
|
||||
t.Errorf("%q. SanitiseString() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitiseString(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
SanitiseString(pen)
|
||||
}
|
||||
}
|
52
common/sort.go
Normal file
52
common/sort.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// SortConfiguration is the configuration of Sort.
|
||||
type SortConfiguration struct {
|
||||
Allowed []string // Allowed parameters
|
||||
Default string
|
||||
DefaultSorting string // if empty, DESC
|
||||
Table string
|
||||
}
|
||||
|
||||
// Sort allows the request to modify how the query is sorted.
|
||||
func Sort(md MethodData, config SortConfiguration) string {
|
||||
if config.DefaultSorting == "" {
|
||||
config.DefaultSorting = "DESC"
|
||||
}
|
||||
if config.Table != "" {
|
||||
config.Table += "."
|
||||
}
|
||||
var sortBy string
|
||||
for _, s := range md.Ctx.Request.URI().QueryArgs().PeekMulti("sort") {
|
||||
sortParts := strings.Split(strings.ToLower(b2s(s)), ",")
|
||||
if contains(config.Allowed, sortParts[0]) {
|
||||
if sortBy != "" {
|
||||
sortBy += ", "
|
||||
}
|
||||
sortBy += config.Table + sortParts[0] + " "
|
||||
if len(sortParts) > 1 && contains([]string{"asc", "desc"}, sortParts[1]) {
|
||||
sortBy += sortParts[1]
|
||||
} else {
|
||||
sortBy += config.DefaultSorting
|
||||
}
|
||||
}
|
||||
}
|
||||
if sortBy == "" {
|
||||
sortBy = config.Default
|
||||
}
|
||||
if sortBy == "" {
|
||||
return ""
|
||||
}
|
||||
return "ORDER BY " + sortBy
|
||||
}
|
||||
|
||||
func contains(a []string, s string) bool {
|
||||
for _, el := range a {
|
||||
if s == el {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@@ -1,9 +1,23 @@
|
||||
package common
|
||||
|
||||
// Token Is an API token.
|
||||
import "fmt"
|
||||
|
||||
// Token is an API token.
|
||||
type Token struct {
|
||||
ID int
|
||||
Value string
|
||||
UserID int
|
||||
Privileges Privileges
|
||||
TokenPrivileges Privileges
|
||||
UserPrivileges UserPrivileges
|
||||
}
|
||||
|
||||
// OnlyUserPublic returns a string containing "(user.privileges & 1 = 1 OR users.id = <userID>)"
|
||||
// if the user does not have the UserPrivilege AdminManageUsers, and returns "1" otherwise.
|
||||
func (t Token) OnlyUserPublic(userManagerSeesEverything bool) string {
|
||||
if userManagerSeesEverything &&
|
||||
t.UserPrivileges&AdminPrivilegeManageUsers == AdminPrivilegeManageUsers {
|
||||
return "1"
|
||||
}
|
||||
// It's safe to use sprintf directly even if it's a query, because UserID is an int.
|
||||
return fmt.Sprintf("(users.privileges & 1 = 1 OR users.id = '%d')", t.UserID)
|
||||
}
|
||||
|
55
common/unix_timestamp.go
Normal file
55
common/unix_timestamp.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UnixTimestamp is simply a time.Time, but can be used to convert an
|
||||
// unix timestamp in the database into a native time.Time.
|
||||
type UnixTimestamp time.Time
|
||||
|
||||
// Scan decodes src into an unix timestamp.
|
||||
func (u *UnixTimestamp) Scan(src interface{}) error {
|
||||
if u == nil {
|
||||
return errors.New("rippleapi/common: UnixTimestamp is nil")
|
||||
}
|
||||
switch src := src.(type) {
|
||||
case int64:
|
||||
*u = UnixTimestamp(time.Unix(src, 0))
|
||||
case float64:
|
||||
*u = UnixTimestamp(time.Unix(int64(src), 0))
|
||||
case string:
|
||||
return u._string(src)
|
||||
case []byte:
|
||||
return u._string(string(src))
|
||||
case nil:
|
||||
// Nothing, leave zero value on timestamp
|
||||
default:
|
||||
return errors.New("rippleapi/common: unhandleable type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UnixTimestamp) _string(s string) error {
|
||||
ts, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = UnixTimestamp(time.Unix(int64(ts), 0))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON -> time.Time.MarshalJSON
|
||||
func (u UnixTimestamp) MarshalJSON() ([]byte, error) {
|
||||
return time.Time(u).MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON -> time.Time.UnmarshalJSON
|
||||
func (u *UnixTimestamp) UnmarshalJSON(x []byte) error {
|
||||
t := new(time.Time)
|
||||
err := t.UnmarshalJSON(x)
|
||||
*u = UnixTimestamp(*t)
|
||||
return err
|
||||
}
|
32
common/update.go
Normal file
32
common/update.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UpdateQuery is simply an SQL update query,
|
||||
// that can be built upon passed parameters.
|
||||
type UpdateQuery struct {
|
||||
fields []string
|
||||
Parameters []interface{}
|
||||
}
|
||||
|
||||
// Add adds a new field with correspective value to UpdateQuery
|
||||
func (u *UpdateQuery) Add(field string, value interface{}) *UpdateQuery {
|
||||
val := reflect.ValueOf(value)
|
||||
if val.Kind() == reflect.Ptr && val.IsNil() {
|
||||
return u
|
||||
}
|
||||
if s, ok := value.(string); ok && s == "" {
|
||||
return u
|
||||
}
|
||||
u.fields = append(u.fields, field+" = ?")
|
||||
u.Parameters = append(u.Parameters, value)
|
||||
return u
|
||||
}
|
||||
|
||||
// Fields retrieves the fields joined by a comma.
|
||||
func (u *UpdateQuery) Fields() string {
|
||||
return strings.Join(u.fields, ", ")
|
||||
}
|
67
common/user_privileges.go
Normal file
67
common/user_privileges.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// user/admin privileges
|
||||
const (
|
||||
UserPrivilegePublic UserPrivileges = 1 << iota
|
||||
UserPrivilegeNormal
|
||||
UserPrivilegeDonor
|
||||
AdminPrivilegeAccessRAP
|
||||
AdminPrivilegeManageUsers
|
||||
AdminPrivilegeBanUsers
|
||||
AdminPrivilegeSilenceUsers
|
||||
AdminPrivilegeWipeUsers
|
||||
AdminPrivilegeManageBeatmap
|
||||
AdminPrivilegeManageServer
|
||||
AdminPrivilegeManageSetting
|
||||
AdminPrivilegeManageBetaKey
|
||||
AdminPrivilegeManageReport
|
||||
AdminPrivilegeManageDocs
|
||||
AdminPrivilegeManageBadges
|
||||
AdminPrivilegeViewRAPLogs
|
||||
AdminPrivilegeManagePrivilege
|
||||
AdminPrivilegeSendAlerts
|
||||
AdminPrivilegeChatMod
|
||||
AdminPrivilegeKickUsers
|
||||
UserPrivilegePendingVerification
|
||||
UserPrivilegeTournamentStaff
|
||||
)
|
||||
|
||||
// UserPrivileges represents a bitwise enum of the privileges of an user.
|
||||
type UserPrivileges uint64
|
||||
|
||||
var userPrivilegeString = [...]string{
|
||||
"UserPublic",
|
||||
"UserNormal",
|
||||
"UserDonor",
|
||||
"AdminAccessRAP",
|
||||
"AdminManageUsers",
|
||||
"AdminBanUsers",
|
||||
"AdminSilenceUsers",
|
||||
"AdminWipeUsers",
|
||||
"AdminManageBeatmap",
|
||||
"AdminManageServer",
|
||||
"AdminManageSetting",
|
||||
"AdminManageBetaKey",
|
||||
"AdminManageReport",
|
||||
"AdminManageDocs",
|
||||
"AdminManageBadges",
|
||||
"AdminViewRAPLogs",
|
||||
"AdminManagePrivilege",
|
||||
"AdminSendAlerts",
|
||||
"AdminChatMod",
|
||||
"AdminKickUsers",
|
||||
"UserPendingVerification",
|
||||
"UserTournamentStaff",
|
||||
}
|
||||
|
||||
func (p UserPrivileges) String() string {
|
||||
var pvs []string
|
||||
for i, v := range userPrivilegeString {
|
||||
if uint64(p)&uint64(1<<uint(i)) != 0 {
|
||||
pvs = append(pvs, v)
|
||||
}
|
||||
}
|
||||
return strings.Join(pvs, ", ")
|
||||
}
|
11
common/utils.go
Normal file
11
common/utils.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SafeUsername makes a string lowercase and replaces all spaces with
|
||||
// underscores.
|
||||
func SafeUsername(s string) string {
|
||||
return strings.Replace(strings.ToLower(s), " ", "_", -1)
|
||||
}
|
20
common/utils_test.go
Normal file
20
common/utils_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSafeUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
want string
|
||||
}{
|
||||
{"noChange", "no_change", "no_change"},
|
||||
{"toLower", "Change_Me", "change_me"},
|
||||
{"complete", "La_M a m m a_putt na", "la_m_a_m_m_a_putt_na"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := SafeUsername(tt.arg); got != tt.want {
|
||||
t.Errorf("%q. SafeUsername() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
91
common/where.go
Normal file
91
common/where.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package common
|
||||
|
||||
// WhereClause is a struct representing a where clause.
|
||||
// This is made to easily create WHERE clauses from parameters passed from a request.
|
||||
type WhereClause struct {
|
||||
Clause string
|
||||
Params []interface{}
|
||||
useOr bool
|
||||
}
|
||||
|
||||
// Where adds a new WHERE clause to the WhereClause.
|
||||
func (w *WhereClause) Where(clause, passedParam string, allowedValues ...string) *WhereClause {
|
||||
if passedParam == "" {
|
||||
return w
|
||||
}
|
||||
if len(allowedValues) != 0 && !contains(allowedValues, passedParam) {
|
||||
return w
|
||||
}
|
||||
w.addWhere()
|
||||
w.Clause += clause
|
||||
w.Params = append(w.Params, passedParam)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *WhereClause) addWhere() {
|
||||
// if string is empty add "WHERE", else add AND
|
||||
if w.Clause == "" {
|
||||
w.Clause += "WHERE "
|
||||
} else {
|
||||
if w.useOr {
|
||||
w.Clause += " OR "
|
||||
return
|
||||
}
|
||||
w.Clause += " AND "
|
||||
}
|
||||
}
|
||||
|
||||
// Or enables using OR instead of AND
|
||||
func (w *WhereClause) Or() *WhereClause {
|
||||
w.useOr = true
|
||||
return w
|
||||
}
|
||||
|
||||
// And enables using AND instead of OR
|
||||
func (w *WhereClause) And() *WhereClause {
|
||||
w.useOr = false
|
||||
return w
|
||||
}
|
||||
|
||||
// In generates an IN clause.
|
||||
// initial is the initial part, e.g. "users.id".
|
||||
// Fields are the possible values.
|
||||
// Sample output: users.id IN ('1', '2', '3')
|
||||
func (w *WhereClause) In(initial string, fields ...[]byte) *WhereClause {
|
||||
if len(fields) == 0 {
|
||||
return w
|
||||
}
|
||||
w.addWhere()
|
||||
w.Clause += initial + " IN (" + generateQuestionMarks(len(fields)) + ")"
|
||||
fieldsInterfaced := make([]interface{}, len(fields))
|
||||
for k, f := range fields {
|
||||
fieldsInterfaced[k] = string(f)
|
||||
}
|
||||
w.Params = append(w.Params, fieldsInterfaced...)
|
||||
return w
|
||||
}
|
||||
|
||||
func generateQuestionMarks(x int) (qm string) {
|
||||
for i := 0; i < x-1; i++ {
|
||||
qm += "?, "
|
||||
}
|
||||
if x > 0 {
|
||||
qm += "?"
|
||||
}
|
||||
return qm
|
||||
}
|
||||
|
||||
// ClauseSafe returns the clause, always containing something. If w.Clause is
|
||||
// empty, it returns "WHERE 1".
|
||||
func (w *WhereClause) ClauseSafe() string {
|
||||
if w.Clause == "" {
|
||||
return "WHERE 1"
|
||||
}
|
||||
return w.Clause
|
||||
}
|
||||
|
||||
// Where is the same as WhereClause.Where, but creates a new WhereClause.
|
||||
func Where(clause, passedParam string, allowedValues ...string) *WhereClause {
|
||||
w := new(WhereClause)
|
||||
return w.Where(clause, passedParam, allowedValues...)
|
||||
}
|
97
common/where_test.go
Normal file
97
common/where_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_generateQuestionMarks(t *testing.T) {
|
||||
type args struct {
|
||||
x int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantQm string
|
||||
}{
|
||||
{"-1", args{-1}, ""},
|
||||
{"0", args{0}, ""},
|
||||
{"1", args{1}, "?"},
|
||||
{"2", args{2}, "?, ?"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if gotQm := generateQuestionMarks(tt.args.x); gotQm != tt.wantQm {
|
||||
t.Errorf("%q. generateQuestionMarks() = %v, want %v", tt.name, gotQm, tt.wantQm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereClause_In(t *testing.T) {
|
||||
type args struct {
|
||||
initial string
|
||||
fields []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields *WhereClause
|
||||
args args
|
||||
want *WhereClause
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
&WhereClause{},
|
||||
args{"users.id", []string{"1", "2", "3"}},
|
||||
&WhereClause{"WHERE users.id IN (?, ?, ?)", []interface{}{"1", "2", "3"}, false},
|
||||
},
|
||||
{
|
||||
"withExisting",
|
||||
Where("users.username = ?", "Howl").Where("users.xd > ?", "6"),
|
||||
args{"users.id", []string{"1"}},
|
||||
&WhereClause{
|
||||
"WHERE users.username = ? AND users.xd > ? AND users.id IN (?)",
|
||||
[]interface{}{"Howl", "6", "1"},
|
||||
false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
w := tt.fields
|
||||
if got := w.In(tt.args.initial, tt.args.fields...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. WhereClause.In() = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhere(t *testing.T) {
|
||||
type args struct {
|
||||
clause string
|
||||
passedParam string
|
||||
allowedValues []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *WhereClause
|
||||
}{
|
||||
{
|
||||
"simple",
|
||||
args{"users.id = ?", "5", nil},
|
||||
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
|
||||
},
|
||||
{
|
||||
"allowed",
|
||||
args{"users.id = ?", "5", []string{"1", "3", "5"}},
|
||||
&WhereClause{"WHERE users.id = ?", []interface{}{"5"}, false},
|
||||
},
|
||||
{
|
||||
"notAllowed",
|
||||
args{"users.id = ?", "5", []string{"0"}},
|
||||
&WhereClause{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := Where(tt.args.clause, tt.args.passedParam, tt.args.allowedValues...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. Where() = %#v, want %#v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
122
limit/limit.go
Normal file
122
limit/limit.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package limit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Request is a Request with DefaultLimiter.
|
||||
func Request(u string, perMinute int) { DefaultLimiter.Request(u, perMinute) }
|
||||
|
||||
// NonBlockingRequest is a NonBlockingRequest with DefaultLimiter.
|
||||
func NonBlockingRequest(u string, perMinute int) bool {
|
||||
return DefaultLimiter.NonBlockingRequest(u, perMinute)
|
||||
}
|
||||
|
||||
// DefaultLimiter is the RateLimiter used by the package-level
|
||||
// Request and NonBlockingRequest.
|
||||
var DefaultLimiter = &RateLimiter{
|
||||
Map: make(map[string]chan struct{}),
|
||||
Mutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// RateLimiter is a simple rate limiter.
|
||||
type RateLimiter struct {
|
||||
Map map[string]chan struct{}
|
||||
Mutex *sync.RWMutex
|
||||
}
|
||||
|
||||
// Request is a simple request. Blocks until it can make the request.
|
||||
func (s *RateLimiter) Request(u string, perMinute int) {
|
||||
s.request(u, perMinute, true)
|
||||
}
|
||||
|
||||
// NonBlockingRequest checks if it can do a request. If it can't, it returns
|
||||
// false, else it returns true if the request succeded.
|
||||
func (s *RateLimiter) NonBlockingRequest(u string, perMinute int) bool {
|
||||
return s.request(u, perMinute, false)
|
||||
}
|
||||
|
||||
func (s *RateLimiter) request(u string, perMinute int, blocking bool) bool {
|
||||
s.check()
|
||||
s.Mutex.RLock()
|
||||
c, exists := s.Map[u]
|
||||
s.Mutex.RUnlock()
|
||||
if !exists {
|
||||
c = makePrefilledChan(perMinute)
|
||||
s.Mutex.Lock()
|
||||
// Now that we have exclusive read and write-access, we want to
|
||||
// make sure we don't overwrite an existing channel. Otherwise,
|
||||
// race conditions and panic happen.
|
||||
if cNew, exists := s.Map[u]; exists {
|
||||
c = cNew
|
||||
s.Mutex.Unlock()
|
||||
} else {
|
||||
s.Map[u] = c
|
||||
s.Mutex.Unlock()
|
||||
<-c
|
||||
go s.filler(u, perMinute)
|
||||
}
|
||||
}
|
||||
return rcv(c, blocking)
|
||||
}
|
||||
|
||||
// rcv receives from a channel, but if blocking is true it waits til something
|
||||
// is received and always returns true, otherwise if it can't receive it
|
||||
// returns false.
|
||||
func rcv(c chan struct{}, blocking bool) bool {
|
||||
if blocking {
|
||||
<-c
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case <-c:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RateLimiter) filler(el string, perMinute int) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
fmt.Println(r)
|
||||
}
|
||||
}()
|
||||
|
||||
s.Mutex.RLock()
|
||||
c := s.Map[el]
|
||||
s.Mutex.RUnlock()
|
||||
for {
|
||||
select {
|
||||
case c <- struct{}{}:
|
||||
time.Sleep(time.Minute / time.Duration(perMinute))
|
||||
default: // c is full
|
||||
s.Mutex.Lock()
|
||||
close(c)
|
||||
delete(s.Map, el)
|
||||
s.Mutex.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check makes sure the map and the mutex are properly initialised.
|
||||
func (s *RateLimiter) check() {
|
||||
if s.Map == nil {
|
||||
s.Map = make(map[string]chan struct{})
|
||||
}
|
||||
if s.Mutex == nil {
|
||||
s.Mutex = new(sync.RWMutex)
|
||||
}
|
||||
}
|
||||
|
||||
func makePrefilledChan(l int) chan struct{} {
|
||||
c := make(chan struct{}, l)
|
||||
for i := 0; i < l; i++ {
|
||||
c <- struct{}{}
|
||||
}
|
||||
return c
|
||||
}
|
119
main.go
119
main.go
@@ -1,28 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.zxq.co/ripple/rippleapi/app"
|
||||
"git.zxq.co/ripple/rippleapi/common"
|
||||
"git.zxq.co/ripple/schiavolib"
|
||||
"github.com/rcrowley/goagain"
|
||||
"zxq.co/ripple/rippleapi/app"
|
||||
"zxq.co/ripple/rippleapi/beatmapget"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
// Golint pls dont break balls
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/serenize/snaker"
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
)
|
||||
|
||||
// Version is the git hash of the application. Do not edit. This is
|
||||
// automatically set using -ldflags during build time.
|
||||
var Version string
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Ltime)
|
||||
log.SetPrefix(fmt.Sprintf("%d|", syscall.Getpid()))
|
||||
common.Version = Version
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
|
||||
func main() {
|
||||
fmt.Print("Ripple API")
|
||||
if Version != "" {
|
||||
fmt.Print("; git commit hash: ", Version)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
conf, halt := common.Load()
|
||||
if halt {
|
||||
return
|
||||
@@ -30,65 +43,45 @@ func main() {
|
||||
|
||||
schiavo.Prefix = "Ripple API"
|
||||
|
||||
db, err := sql.Open(conf.DatabaseType, conf.DSN)
|
||||
if !strings.Contains(conf.DSN, "parseTime=true") {
|
||||
c := "?"
|
||||
if strings.Contains(conf.DSN, "?") {
|
||||
c = "&"
|
||||
}
|
||||
conf.DSN += c + "parseTime=true&charset=utf8mb4,utf8&collation=utf8mb4_general_ci"
|
||||
}
|
||||
|
||||
var err error
|
||||
db, err = sqlx.Open(conf.DatabaseType, conf.DSN)
|
||||
if err != nil {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
db.MapperFunc(func(s string) string {
|
||||
if x, ok := commonClusterfucks[s]; ok {
|
||||
return x
|
||||
}
|
||||
return snaker.CamelToSnake(s)
|
||||
})
|
||||
|
||||
beatmapget.Client = osuapi.NewClient(conf.OsuAPIKey)
|
||||
beatmapget.DB = db
|
||||
|
||||
engine := app.Start(conf, db)
|
||||
|
||||
// Inherit a net.Listener from our parent process or listen anew.
|
||||
l, err := goagain.Listener()
|
||||
if nil != err {
|
||||
|
||||
// Listen on a TCP or a UNIX domain socket (TCP here).
|
||||
if conf.Unix {
|
||||
l, err = net.Listen("unix", conf.ListenTo)
|
||||
} else {
|
||||
l, err = net.Listen("tcp", conf.ListenTo)
|
||||
}
|
||||
if nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr()))
|
||||
|
||||
// Accept connections in a new goroutine.
|
||||
go http.Serve(l, engine)
|
||||
|
||||
} else {
|
||||
|
||||
// Resume accepting connections in a new goroutine.
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr()))
|
||||
go http.Serve(l, engine)
|
||||
|
||||
// Kill the parent, now that the child has started successfully.
|
||||
if err := goagain.Kill(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Block the main goroutine awaiting signals.
|
||||
if _, err := goagain.Wait(l); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Do whatever's necessary to ensure a graceful exit like waiting for
|
||||
// goroutines to terminate or a channel to become closed.
|
||||
//
|
||||
// In this case, we'll simply stop listening and wait one second.
|
||||
if err := l.Close(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
startuato(engine.Handler)
|
||||
}
|
||||
|
||||
var commonClusterfucks = map[string]string{
|
||||
"RegisteredOn": "register_datetime",
|
||||
"UsernameAKA": "username_aka",
|
||||
"BeatmapMD5": "beatmap_md5",
|
||||
"Count300": "300_count",
|
||||
"Count100": "100_count",
|
||||
"Count50": "50_count",
|
||||
"CountGeki": "gekis_count",
|
||||
"CountKatu": "katus_count",
|
||||
"CountMiss": "misses_count",
|
||||
"PP": "pp",
|
||||
}
|
||||
|
71
startuato_linux.go
Normal file
71
startuato_linux.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/rcrowley/goagain"
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"zxq.co/ripple/schiavolib"
|
||||
)
|
||||
|
||||
func startuato(hn fasthttp.RequestHandler) {
|
||||
conf, _ := common.Load()
|
||||
// Inherit a net.Listener from our parent process or listen anew.
|
||||
l, err := goagain.Listener()
|
||||
if nil != err {
|
||||
|
||||
// Listen on a TCP or a UNIX domain socket (TCP here).
|
||||
if conf.Unix {
|
||||
l, err = net.Listen("unix", conf.ListenTo)
|
||||
} else {
|
||||
l, err = net.Listen("tcp", conf.ListenTo)
|
||||
}
|
||||
if nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU STARTUATO ON ", l.Addr()))
|
||||
|
||||
// Accept connections in a new goroutine.
|
||||
go fasthttp.Serve(l, hn)
|
||||
} else {
|
||||
|
||||
// Resume accepting connections in a new goroutine.
|
||||
schiavo.Bunker.Send(fmt.Sprint("LISTENINGU RESUMINGU ON ", l.Addr()))
|
||||
go fasthttp.Serve(l, hn)
|
||||
|
||||
// Kill the parent, now that the child has started successfully.
|
||||
if err := goagain.Kill(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Block the main goroutine awaiting signals.
|
||||
if _, err := goagain.Wait(l); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Do whatever's necessary to ensure a graceful exit like waiting for
|
||||
// goroutines to terminate or a channel to become closed.
|
||||
//
|
||||
// In this case, we'll simply stop listening and wait one second.
|
||||
if err := l.Close(); nil != err {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
schiavo.Bunker.Send(err.Error())
|
||||
log.Fatalln(err)
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
}
|
34
startuato_windows.go
Normal file
34
startuato_windows.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// +build windows
|
||||
|
||||
// The Ripple API on Windows is not officially supported and you're probably
|
||||
// gonna swear a lot if you intend to use it on Windows. Caveat emptor.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
)
|
||||
|
||||
func startuato(hn fasthttp.RequestHandler) {
|
||||
conf, _ := common.Load()
|
||||
var (
|
||||
l net.Listener
|
||||
err error
|
||||
)
|
||||
|
||||
// Listen on a TCP or a UNIX domain socket.
|
||||
if conf.Unix {
|
||||
l, err = net.Listen("unix", conf.ListenTo)
|
||||
} else {
|
||||
l, err = net.Listen("tcp", conf.ListenTo)
|
||||
}
|
||||
if nil != err {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fasthttp.Serve(l, hn)
|
||||
}
|
19
vendor/github.com/DataDog/datadog-go/LICENSE.txt
generated
vendored
Normal file
19
vendor/github.com/DataDog/datadog-go/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015 Datadog, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
52
vendor/github.com/DataDog/datadog-go/statsd/README.md
generated
vendored
Normal file
52
vendor/github.com/DataDog/datadog-go/statsd/README.md
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
## Overview
|
||||
|
||||
Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags
|
||||
and histograms.
|
||||
|
||||
## Get the code
|
||||
|
||||
$ go get github.com/DataDog/datadog-go/statsd
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
// Create the client
|
||||
c, err := statsd.New("127.0.0.1:8125")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Prefix every metric with the app name
|
||||
c.Namespace = "flubber."
|
||||
// Send the EC2 availability zone as a tag with every metric
|
||||
c.Tags = append(c.Tags, "us-east-1a")
|
||||
|
||||
// Do some metrics!
|
||||
err = c.Gauge("request.queue_depth", 12, nil, 1)
|
||||
err = c.Timing("request.duration", duration, nil, 1) // Uses a time.Duration!
|
||||
err = c.TimeInMilliseconds("request", 12, nil, 1)
|
||||
err = c.Incr("request.count_total", nil, 1)
|
||||
err = c.Decr("request.count_total", nil, 1)
|
||||
err = c.Count("request.count_total", 2, nil, 1)
|
||||
```
|
||||
|
||||
## Buffering Client
|
||||
|
||||
DogStatsD accepts packets with multiple statsd payloads in them. Using the BufferingClient via `NewBufferingClient` will buffer up commands and send them when the buffer is reached or after 100msec.
|
||||
|
||||
## Development
|
||||
|
||||
Run the tests with:
|
||||
|
||||
$ go test
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see: http://godoc.org/github.com/DataDog/datadog-go/statsd
|
||||
|
||||
## License
|
||||
|
||||
go-dogstatsd is released under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
|
||||
|
||||
## Credits
|
||||
|
||||
Original code by [ooyala](https://github.com/ooyala/go-dogstatsd).
|
577
vendor/github.com/DataDog/datadog-go/statsd/statsd.go
generated
vendored
Normal file
577
vendor/github.com/DataDog/datadog-go/statsd/statsd.go
generated
vendored
Normal file
@@ -0,0 +1,577 @@
|
||||
// Copyright 2013 Ooyala, Inc.
|
||||
|
||||
/*
|
||||
Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd,
|
||||
adding tags and histograms and pushing upstream to Datadog.
|
||||
|
||||
Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD.
|
||||
|
||||
Example Usage:
|
||||
|
||||
// Create the client
|
||||
c, err := statsd.New("127.0.0.1:8125")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Prefix every metric with the app name
|
||||
c.Namespace = "flubber."
|
||||
// Send the EC2 availability zone as a tag with every metric
|
||||
c.Tags = append(c.Tags, "us-east-1a")
|
||||
err = c.Gauge("request.duration", 1.2, nil, 1)
|
||||
|
||||
statsd is based on go-statsd-client.
|
||||
*/
|
||||
package statsd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
OptimalPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes
|
||||
is optimal for regular networks with an MTU of 1500 so datagrams don't get
|
||||
fragmented. It's generally recommended not to fragment UDP datagrams as losing
|
||||
a single fragment will cause the entire datagram to be lost.
|
||||
|
||||
This can be increased if your network has a greater MTU or you don't mind UDP
|
||||
datagrams getting fragmented. The practical limit is MaxUDPPayloadSize
|
||||
*/
|
||||
const OptimalPayloadSize = 1432
|
||||
|
||||
/*
|
||||
MaxUDPPayloadSize defines the maximum payload size for a UDP datagram.
|
||||
Its value comes from the calculation: 65535 bytes Max UDP datagram size -
|
||||
8byte UDP header - 60byte max IP headers
|
||||
any number greater than that will see frames being cut out.
|
||||
*/
|
||||
const MaxUDPPayloadSize = 65467
|
||||
|
||||
// A Client is a handle for sending udp messages to dogstatsd. It is safe to
|
||||
// use one Client from multiple goroutines simultaneously.
|
||||
type Client struct {
|
||||
conn net.Conn
|
||||
// Namespace to prepend to all statsd calls
|
||||
Namespace string
|
||||
// Tags are global tags to be added to every statsd call
|
||||
Tags []string
|
||||
// BufferLength is the length of the buffer in commands.
|
||||
bufferLength int
|
||||
flushTime time.Duration
|
||||
commands []string
|
||||
buffer bytes.Buffer
|
||||
stop bool
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// New returns a pointer to a new Client given an addr in the format "hostname:port".
|
||||
func New(addr string) (*Client, error) {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := net.DialUDP("udp", nil, udpAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &Client{conn: conn}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewBuffered returns a Client that buffers its output and sends it in chunks.
|
||||
// Buflen is the length of the buffer in number of commands.
|
||||
func NewBuffered(addr string, buflen int) (*Client, error) {
|
||||
client, err := New(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.bufferLength = buflen
|
||||
client.commands = make([]string, 0, buflen)
|
||||
client.flushTime = time.Millisecond * 100
|
||||
go client.watch()
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// format a message from its name, value, tags and rate. Also adds global
|
||||
// namespace and tags.
|
||||
func (c *Client) format(name, value string, tags []string, rate float64) string {
|
||||
var buf bytes.Buffer
|
||||
if c.Namespace != "" {
|
||||
buf.WriteString(c.Namespace)
|
||||
}
|
||||
buf.WriteString(name)
|
||||
buf.WriteString(":")
|
||||
buf.WriteString(value)
|
||||
if rate < 1 {
|
||||
buf.WriteString(`|@`)
|
||||
buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64))
|
||||
}
|
||||
|
||||
writeTagString(&buf, c.Tags, tags)
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (c *Client) watch() {
|
||||
for _ = range time.Tick(c.flushTime) {
|
||||
if c.stop {
|
||||
return
|
||||
}
|
||||
c.Lock()
|
||||
if len(c.commands) > 0 {
|
||||
// FIXME: eating error here
|
||||
c.flush()
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) append(cmd string) error {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.commands = append(c.commands, cmd)
|
||||
// if we should flush, lets do it
|
||||
if len(c.commands) == c.bufferLength {
|
||||
if err := c.flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) joinMaxSize(cmds []string, sep string, maxSize int) ([][]byte, []int) {
|
||||
c.buffer.Reset() //clear buffer
|
||||
|
||||
var frames [][]byte
|
||||
var ncmds []int
|
||||
sepBytes := []byte(sep)
|
||||
sepLen := len(sep)
|
||||
|
||||
elem := 0
|
||||
for _, cmd := range cmds {
|
||||
needed := len(cmd)
|
||||
|
||||
if elem != 0 {
|
||||
needed = needed + sepLen
|
||||
}
|
||||
|
||||
if c.buffer.Len()+needed <= maxSize {
|
||||
if elem != 0 {
|
||||
c.buffer.Write(sepBytes)
|
||||
}
|
||||
c.buffer.WriteString(cmd)
|
||||
elem++
|
||||
} else {
|
||||
frames = append(frames, copyAndResetBuffer(&c.buffer))
|
||||
ncmds = append(ncmds, elem)
|
||||
// if cmd is bigger than maxSize it will get flushed on next loop
|
||||
c.buffer.WriteString(cmd)
|
||||
elem = 1
|
||||
}
|
||||
}
|
||||
|
||||
//add whatever is left! if there's actually something
|
||||
if c.buffer.Len() > 0 {
|
||||
frames = append(frames, copyAndResetBuffer(&c.buffer))
|
||||
ncmds = append(ncmds, elem)
|
||||
}
|
||||
|
||||
return frames, ncmds
|
||||
}
|
||||
|
||||
func copyAndResetBuffer(buf *bytes.Buffer) []byte {
|
||||
tmpBuf := make([]byte, buf.Len())
|
||||
copy(tmpBuf, buf.Bytes())
|
||||
buf.Reset()
|
||||
return tmpBuf
|
||||
}
|
||||
|
||||
// flush the commands in the buffer. Lock must be held by caller.
|
||||
func (c *Client) flush() error {
|
||||
frames, flushable := c.joinMaxSize(c.commands, "\n", OptimalPayloadSize)
|
||||
var err error
|
||||
cmdsFlushed := 0
|
||||
for i, data := range frames {
|
||||
_, e := c.conn.Write(data)
|
||||
if e != nil {
|
||||
err = e
|
||||
break
|
||||
}
|
||||
cmdsFlushed += flushable[i]
|
||||
}
|
||||
|
||||
// clear the slice with a slice op, doesn't realloc
|
||||
if cmdsFlushed == len(c.commands) {
|
||||
c.commands = c.commands[:0]
|
||||
} else {
|
||||
//this case will cause a future realloc...
|
||||
// drop problematic command though (sorry).
|
||||
c.commands = c.commands[cmdsFlushed+1:]
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) sendMsg(msg string) error {
|
||||
// return an error if message is bigger than MaxUDPPayloadSize
|
||||
if len(msg) > MaxUDPPayloadSize {
|
||||
return errors.New("message size exceeds MaxUDPPayloadSize")
|
||||
}
|
||||
|
||||
// if this client is buffered, then we'll just append this
|
||||
if c.bufferLength > 0 {
|
||||
return c.append(msg)
|
||||
}
|
||||
|
||||
_, err := c.conn.Write([]byte(msg))
|
||||
return err
|
||||
}
|
||||
|
||||
// send handles sampling and sends the message over UDP. It also adds global namespace prefixes and tags.
|
||||
func (c *Client) send(name, value string, tags []string, rate float64) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if rate < 1 && rand.Float64() > rate {
|
||||
return nil
|
||||
}
|
||||
data := c.format(name, value, tags, rate)
|
||||
return c.sendMsg(data)
|
||||
}
|
||||
|
||||
// Gauge measures the value of a metric at a particular time.
|
||||
func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error {
|
||||
stat := fmt.Sprintf("%f|g", value)
|
||||
return c.send(name, stat, tags, rate)
|
||||
}
|
||||
|
||||
// Count tracks how many times something happened per second.
|
||||
func (c *Client) Count(name string, value int64, tags []string, rate float64) error {
|
||||
stat := fmt.Sprintf("%d|c", value)
|
||||
return c.send(name, stat, tags, rate)
|
||||
}
|
||||
|
||||
// Histogram tracks the statistical distribution of a set of values.
|
||||
func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error {
|
||||
stat := fmt.Sprintf("%f|h", value)
|
||||
return c.send(name, stat, tags, rate)
|
||||
}
|
||||
|
||||
// Decr is just Count of 1
|
||||
func (c *Client) Decr(name string, tags []string, rate float64) error {
|
||||
return c.send(name, "-1|c", tags, rate)
|
||||
}
|
||||
|
||||
// Incr is just Count of 1
|
||||
func (c *Client) Incr(name string, tags []string, rate float64) error {
|
||||
return c.send(name, "1|c", tags, rate)
|
||||
}
|
||||
|
||||
// Set counts the number of unique elements in a group.
|
||||
func (c *Client) Set(name string, value string, tags []string, rate float64) error {
|
||||
stat := fmt.Sprintf("%s|s", value)
|
||||
return c.send(name, stat, tags, rate)
|
||||
}
|
||||
|
||||
// Timing sends timing information, it is an alias for TimeInMilliseconds
|
||||
func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error {
|
||||
return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate)
|
||||
}
|
||||
|
||||
// TimeInMilliseconds sends timing information in milliseconds.
|
||||
// It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing)
|
||||
func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error {
|
||||
stat := fmt.Sprintf("%f|ms", value)
|
||||
return c.send(name, stat, tags, rate)
|
||||
}
|
||||
|
||||
// Event sends the provided Event.
|
||||
func (c *Client) Event(e *Event) error {
|
||||
stat, err := e.Encode(c.Tags...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.sendMsg(stat)
|
||||
}
|
||||
|
||||
// SimpleEvent sends an event with the provided title and text.
|
||||
func (c *Client) SimpleEvent(title, text string) error {
|
||||
e := NewEvent(title, text)
|
||||
return c.Event(e)
|
||||
}
|
||||
|
||||
// ServiceCheck sends the provided ServiceCheck.
|
||||
func (c *Client) ServiceCheck(sc *ServiceCheck) error {
|
||||
stat, err := sc.Encode(c.Tags...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.sendMsg(stat)
|
||||
}
|
||||
|
||||
// SimpleServiceCheck sends an serviceCheck with the provided name and status.
|
||||
func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error {
|
||||
sc := NewServiceCheck(name, status)
|
||||
return c.ServiceCheck(sc)
|
||||
}
|
||||
|
||||
// Close the client connection.
|
||||
func (c *Client) Close() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
c.stop = true
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// Events support
|
||||
|
||||
type eventAlertType string
|
||||
|
||||
const (
|
||||
// Info is the "info" AlertType for events
|
||||
Info eventAlertType = "info"
|
||||
// Error is the "error" AlertType for events
|
||||
Error eventAlertType = "error"
|
||||
// Warning is the "warning" AlertType for events
|
||||
Warning eventAlertType = "warning"
|
||||
// Success is the "success" AlertType for events
|
||||
Success eventAlertType = "success"
|
||||
)
|
||||
|
||||
type eventPriority string
|
||||
|
||||
const (
|
||||
// Normal is the "normal" Priority for events
|
||||
Normal eventPriority = "normal"
|
||||
// Low is the "low" Priority for events
|
||||
Low eventPriority = "low"
|
||||
)
|
||||
|
||||
// An Event is an object that can be posted to your DataDog event stream.
|
||||
type Event struct {
|
||||
// Title of the event. Required.
|
||||
Title string
|
||||
// Text is the description of the event. Required.
|
||||
Text string
|
||||
// Timestamp is a timestamp for the event. If not provided, the dogstatsd
|
||||
// server will set this to the current time.
|
||||
Timestamp time.Time
|
||||
// Hostname for the event.
|
||||
Hostname string
|
||||
// AggregationKey groups this event with others of the same key.
|
||||
AggregationKey string
|
||||
// Priority of the event. Can be statsd.Low or statsd.Normal.
|
||||
Priority eventPriority
|
||||
// SourceTypeName is a source type for the event.
|
||||
SourceTypeName string
|
||||
// AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success.
|
||||
// If absent, the default value applied by the dogstatsd server is Info.
|
||||
AlertType eventAlertType
|
||||
// Tags for the event.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// NewEvent creates a new event with the given title and text. Error checking
|
||||
// against these values is done at send-time, or upon running e.Check.
|
||||
func NewEvent(title, text string) *Event {
|
||||
return &Event{
|
||||
Title: title,
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// Check verifies that an event is valid.
|
||||
func (e Event) Check() error {
|
||||
if len(e.Title) == 0 {
|
||||
return fmt.Errorf("statsd.Event title is required")
|
||||
}
|
||||
if len(e.Text) == 0 {
|
||||
return fmt.Errorf("statsd.Event text is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode returns the dogstatsd wire protocol representation for an event.
|
||||
// Tags may be passed which will be added to the encoded output but not to
|
||||
// the Event's list of tags, eg. for default tags.
|
||||
func (e Event) Encode(tags ...string) (string, error) {
|
||||
err := e.Check()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := e.escapedText()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString("_e{")
|
||||
buffer.WriteString(strconv.FormatInt(int64(len(e.Title)), 10))
|
||||
buffer.WriteRune(',')
|
||||
buffer.WriteString(strconv.FormatInt(int64(len(text)), 10))
|
||||
buffer.WriteString("}:")
|
||||
buffer.WriteString(e.Title)
|
||||
buffer.WriteRune('|')
|
||||
buffer.WriteString(text)
|
||||
|
||||
if !e.Timestamp.IsZero() {
|
||||
buffer.WriteString("|d:")
|
||||
buffer.WriteString(strconv.FormatInt(int64(e.Timestamp.Unix()), 10))
|
||||
}
|
||||
|
||||
if len(e.Hostname) != 0 {
|
||||
buffer.WriteString("|h:")
|
||||
buffer.WriteString(e.Hostname)
|
||||
}
|
||||
|
||||
if len(e.AggregationKey) != 0 {
|
||||
buffer.WriteString("|k:")
|
||||
buffer.WriteString(e.AggregationKey)
|
||||
|
||||
}
|
||||
|
||||
if len(e.Priority) != 0 {
|
||||
buffer.WriteString("|p:")
|
||||
buffer.WriteString(string(e.Priority))
|
||||
}
|
||||
|
||||
if len(e.SourceTypeName) != 0 {
|
||||
buffer.WriteString("|s:")
|
||||
buffer.WriteString(e.SourceTypeName)
|
||||
}
|
||||
|
||||
if len(e.AlertType) != 0 {
|
||||
buffer.WriteString("|t:")
|
||||
buffer.WriteString(string(e.AlertType))
|
||||
}
|
||||
|
||||
writeTagString(&buffer, tags, e.Tags)
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
// ServiceCheck support
|
||||
|
||||
type ServiceCheckStatus byte
|
||||
|
||||
const (
|
||||
// Ok is the "ok" ServiceCheck status
|
||||
Ok ServiceCheckStatus = 0
|
||||
// Warn is the "warning" ServiceCheck status
|
||||
Warn ServiceCheckStatus = 1
|
||||
// Critical is the "critical" ServiceCheck status
|
||||
Critical ServiceCheckStatus = 2
|
||||
// Unknown is the "unknown" ServiceCheck status
|
||||
Unknown ServiceCheckStatus = 3
|
||||
)
|
||||
|
||||
// An ServiceCheck is an object that contains status of DataDog service check.
|
||||
type ServiceCheck struct {
|
||||
// Name of the service check. Required.
|
||||
Name string
|
||||
// Status of service check. Required.
|
||||
Status ServiceCheckStatus
|
||||
// Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd
|
||||
// server will set this to the current time.
|
||||
Timestamp time.Time
|
||||
// Hostname for the serviceCheck.
|
||||
Hostname string
|
||||
// A message describing the current state of the serviceCheck.
|
||||
Message string
|
||||
// Tags for the serviceCheck.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// NewServiceCheck creates a new serviceCheck with the given name and status. Error checking
|
||||
// against these values is done at send-time, or upon running sc.Check.
|
||||
func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck {
|
||||
return &ServiceCheck{
|
||||
Name: name,
|
||||
Status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// Check verifies that an event is valid.
|
||||
func (sc ServiceCheck) Check() error {
|
||||
if len(sc.Name) == 0 {
|
||||
return fmt.Errorf("statsd.ServiceCheck name is required")
|
||||
}
|
||||
if byte(sc.Status) < 0 || byte(sc.Status) > 3 {
|
||||
return fmt.Errorf("statsd.ServiceCheck status has invalid value")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encode returns the dogstatsd wire protocol representation for an serviceCheck.
|
||||
// Tags may be passed which will be added to the encoded output but not to
|
||||
// the Event's list of tags, eg. for default tags.
|
||||
func (sc ServiceCheck) Encode(tags ...string) (string, error) {
|
||||
err := sc.Check()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
message := sc.escapedMessage()
|
||||
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString("_sc|")
|
||||
buffer.WriteString(sc.Name)
|
||||
buffer.WriteRune('|')
|
||||
buffer.WriteString(strconv.FormatInt(int64(sc.Status), 10))
|
||||
|
||||
if !sc.Timestamp.IsZero() {
|
||||
buffer.WriteString("|d:")
|
||||
buffer.WriteString(strconv.FormatInt(int64(sc.Timestamp.Unix()), 10))
|
||||
}
|
||||
|
||||
if len(sc.Hostname) != 0 {
|
||||
buffer.WriteString("|h:")
|
||||
buffer.WriteString(sc.Hostname)
|
||||
}
|
||||
|
||||
writeTagString(&buffer, tags, sc.Tags)
|
||||
|
||||
if len(message) != 0 {
|
||||
buffer.WriteString("|m:")
|
||||
buffer.WriteString(message)
|
||||
}
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func (e Event) escapedText() string {
|
||||
return strings.Replace(e.Text, "\n", "\\n", -1)
|
||||
}
|
||||
|
||||
func (sc ServiceCheck) escapedMessage() string {
|
||||
msg := strings.Replace(sc.Message, "\n", "\\n", -1)
|
||||
return strings.Replace(msg, "m:", `m\:`, -1)
|
||||
}
|
||||
|
||||
func removeNewlines(str string) string {
|
||||
return strings.Replace(str, "\n", "", -1)
|
||||
}
|
||||
|
||||
func writeTagString(w io.Writer, tagList1, tagList2 []string) {
|
||||
// the tag lists may be shared with other callers, so we cannot modify
|
||||
// them in any way (which means we cannot append to them either)
|
||||
// therefore we must make an entirely separate copy just for this call
|
||||
totalLen := len(tagList1) + len(tagList2)
|
||||
if totalLen == 0 {
|
||||
return
|
||||
}
|
||||
tags := make([]string, 0, totalLen)
|
||||
tags = append(tags, tagList1...)
|
||||
tags = append(tags, tagList2...)
|
||||
|
||||
io.WriteString(w, "|#")
|
||||
io.WriteString(w, removeNewlines(tags[0]))
|
||||
for _, tag := range tags[1:] {
|
||||
io.WriteString(w, ",")
|
||||
io.WriteString(w, removeNewlines(tag))
|
||||
}
|
||||
}
|
24
vendor/github.com/buaazp/fasthttprouter/HttpRouterLicense
generated
vendored
Normal file
24
vendor/github.com/buaazp/fasthttprouter/HttpRouterLicense
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
Copyright (c) 2013 Julien Schmidt. All rights reserved.
|
||||
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* The names of the contributors may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
vendor/github.com/buaazp/fasthttprouter/LICENSE
generated
vendored
Normal file
28
vendor/github.com/buaazp/fasthttprouter/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2015-2016, 招牌疯子
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of uq nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
216
vendor/github.com/buaazp/fasthttprouter/README.md
generated
vendored
Normal file
216
vendor/github.com/buaazp/fasthttprouter/README.md
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
# FastHttpRouter
|
||||
[](https://travis-ci.org/buaazp/fasthttprouter)
|
||||
[](https://coveralls.io/github/buaazp/fasthttprouter?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/buaazp/fasthttprouter)
|
||||
[](http://godoc.org/github.com/buaazp/fasthttprouter)
|
||||
[](https://github.com/buaazp/fasthttprouter/releases)
|
||||
|
||||
FastHttpRouter is forked from [httprouter](https://github.com/julienschmidt/httprouter) which is a lightweight high performance HTTP request router
|
||||
(also called *multiplexer* or just *mux* for short) for [fasthttp](https://github.com/valyala/fasthttp).
|
||||
|
||||
This router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.
|
||||
|
||||
#### License Related
|
||||
|
||||
- The author of `httprouter` [@julienschmidt](https://github.com/julienschmidt) did almost all the hard work of this router.
|
||||
- I respect the laws of open source. So LICENSE of `httprouter` is alway stay here: [HttpRouterLicense](HttpRouterLicense).
|
||||
- What I do is just fit for `fasthttp`. I have no hope to build a huge but toxic go web framwork like [iris](https://github.com/kataras/iris).
|
||||
- I fork this repo is just because there is no router for `fasthttp` at that time. And `fasthttprouter` is the FIRST router for `fasthttp`.
|
||||
- `fasthttprouter` has been used in my online production and processes 17 million requests per day. It is fast and stable, so I decide to release a stable version.
|
||||
|
||||
#### Releases
|
||||
|
||||
- [2016.10.24] [v0.1.0](https://github.com/buaazp/fasthttprouter/releases/tag/v0.1.0) The first release version of `fasthttprouter`.
|
||||
|
||||
## Features
|
||||
|
||||
**Best Performance:** FastHttpRouter is **one of the fastest** go web frameworks in the [go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark). Even faster than httprouter itself.
|
||||
|
||||
- Basic Test: The first test case is to mock 0 ms, 10 ms, 100 ms, 500 ms processing time in handlers. The concurrency clients are 5000.
|
||||
|
||||

|
||||
|
||||
- Concurrency Test: In 30 ms processing time, the tets result for 100, 1000, 5000 clients is:
|
||||
|
||||

|
||||
|
||||
See below for technical details of the implementation.
|
||||
|
||||
**Only explicit matches:** With other routers, like [http.ServeMux](http://golang.org/pkg/net/http/#ServeMux),
|
||||
a requested URL path could match multiple patterns. Therefore they have some
|
||||
awkward pattern priority rules, like *longest match* or *first registered,
|
||||
first matched*. By design of this router, a request can only match exactly one
|
||||
or no route. As a result, there are also no unintended matches, which makes it
|
||||
great for SEO and improves the user experience.
|
||||
|
||||
**Stop caring about trailing slashes:** Choose the URL style you like, the
|
||||
router automatically redirects the client if a trailing slash is missing or if
|
||||
there is one extra. Of course it only does so, if the new path has a handler.
|
||||
If you don't like it, you can [turn off this behavior](http://godoc.org/github.com/buaazp/fasthttprouter#Router.RedirectTrailingSlash).
|
||||
|
||||
**Path auto-correction:** Besides detecting the missing or additional trailing
|
||||
slash at no extra cost, the router can also fix wrong cases and remove
|
||||
superfluous path elements (like `../` or `//`).
|
||||
Is [CAPTAIN CAPS LOCK](http://www.urbandictionary.com/define.php?term=Captain+Caps+Lock) one of your users?
|
||||
FastHttpRouter can help him by making a case-insensitive look-up and redirecting him
|
||||
to the correct URL.
|
||||
|
||||
**Parameters in your routing pattern:** Stop parsing the requested URL path,
|
||||
just give the path segment a name and the router delivers the dynamic value to
|
||||
you. Because of the design of the router, path parameters are very cheap.
|
||||
|
||||
**Zero Garbage:** The matching and dispatching process generates zero bytes of
|
||||
garbage. In fact, the only heap allocations that are made, is by building the
|
||||
slice of the key-value pairs for path parameters. If the request path contains
|
||||
no parameters, not a single heap allocation is necessary.
|
||||
|
||||
**No more server crashes:** You can set a [Panic handler](http://godoc.org/github.com/buaazp/fasthttprouter#Router.PanicHandler) to deal with panics
|
||||
occurring during handling a HTTP request. The router then recovers and lets the
|
||||
PanicHandler log what happened and deliver a nice error page.
|
||||
|
||||
**Perfect for APIs:** The router design encourages to build sensible, hierarchical
|
||||
RESTful APIs. Moreover it has builtin native support for [OPTIONS requests](http://zacstewart.com/2012/04/14/http-options-method.html)
|
||||
and `405 Method Not Allowed` replies.
|
||||
|
||||
Of course you can also set **custom [NotFound](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) and [MethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.MethodNotAllowed) handlers** and [**serve static files**](http://godoc.org/github.com/buaazp/fasthttprouter#Router.ServeFiles).
|
||||
|
||||
## Usage
|
||||
|
||||
This is just a quick introduction, view the [GoDoc](http://godoc.org/github.com/buaazp/fasthttprouter) for details:
|
||||
|
||||
Let's start with a trivial example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/buaazp/fasthttprouter"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Index(ctx *fasthttp.RequestCtx) {
|
||||
fmt.Fprint(ctx, "Welcome!\n")
|
||||
}
|
||||
|
||||
func Hello(ctx *fasthttp.RequestCtx) {
|
||||
fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
router := fasthttprouter.New()
|
||||
router.GET("/", Index)
|
||||
router.GET("/hello/:name", Hello)
|
||||
|
||||
log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler))
|
||||
}
|
||||
```
|
||||
|
||||
### Named parameters
|
||||
|
||||
As you can see, `:name` is a *named parameter*. The values are accessible via `RequestCtx.UserValues`. You can get the value of a parameter by using the `ctx.UserValue("name")`.
|
||||
|
||||
Named parameters only match a single path segment:
|
||||
|
||||
```
|
||||
Pattern: /user/:user
|
||||
|
||||
/user/gordon match
|
||||
/user/you match
|
||||
/user/gordon/profile no match
|
||||
/user/ no match
|
||||
```
|
||||
|
||||
**Note:** Since this router has only explicit matches, you can not register static routes and parameters for the same path segment. For example you can not register the patterns `/user/new` and `/user/:user` for the same request method at the same time. The routing of different request methods is independent from each other.
|
||||
|
||||
### Catch-All parameters
|
||||
|
||||
The second type are *catch-all* parameters and have the form `*name`.
|
||||
Like the name suggests, they match everything.
|
||||
Therefore they must always be at the **end** of the pattern:
|
||||
|
||||
```
|
||||
Pattern: /src/*filepath
|
||||
|
||||
/src/ match
|
||||
/src/somefile.go match
|
||||
/src/subdir/somefile.go match
|
||||
```
|
||||
|
||||
## How does it work?
|
||||
|
||||
The router relies on a tree structure which makes heavy use of *common prefixes*, it is basically a *compact* [*prefix tree*](https://en.wikipedia.org/wiki/Trie) (or just [*Radix tree*](https://en.wikipedia.org/wiki/Radix_tree)). Nodes with a common prefix also share a common parent. Here is a short example what the routing tree for the `GET` request method could look like:
|
||||
|
||||
```
|
||||
Priority Path Handle
|
||||
9 \ *<1>
|
||||
3 ├s nil
|
||||
2 |├earch\ *<2>
|
||||
1 |└upport\ *<3>
|
||||
2 ├blog\ *<4>
|
||||
1 | └:post nil
|
||||
1 | └\ *<5>
|
||||
2 ├about-us\ *<6>
|
||||
1 | └team\ *<7>
|
||||
1 └contact\ *<8>
|
||||
```
|
||||
|
||||
Every `*<num>` represents the memory address of a handler function (a pointer). If you follow a path trough the tree from the root to the leaf, you get the complete route path, e.g `\blog\:post\`, where `:post` is just a placeholder ([*parameter*](#named-parameters)) for an actual post name. Unlike hash-maps, a tree structure also allows us to use dynamic parts like the `:post` parameter, since we actually match against the routing patterns instead of just comparing hashes. [As benchmarks show][benchmark], this works very well and efficient.
|
||||
|
||||
Since URL paths have a hierarchical structure and make use only of a limited set of characters (byte values), it is very likely that there are a lot of common prefixes. This allows us to easily reduce the routing into ever smaller problems. Moreover the router manages a separate tree for every request method. For one thing it is more space efficient than holding a method->handle map in every single node, for another thing is also allows us to greatly reduce the routing problem before even starting the look-up in the prefix-tree.
|
||||
|
||||
For even better scalability, the child nodes on each tree level are ordered by priority, where the priority is just the number of handles registered in sub nodes (children, grandchildren, and so on..). This helps in two ways:
|
||||
|
||||
1. Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
|
||||
2. It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.
|
||||
|
||||
```
|
||||
├------------
|
||||
├---------
|
||||
├-----
|
||||
├----
|
||||
├--
|
||||
├--
|
||||
└-
|
||||
```
|
||||
|
||||
## Why doesn't this work with `http.Handler`?
|
||||
|
||||
Becasue fasthttp doesn't provide http.Handler. See this [description](https://github.com/valyala/fasthttp#switching-from-nethttp-to-fasthttp).
|
||||
|
||||
Fasthttp works with [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler) functions instead of objects implementing Handler interface. So a FastHttpRouter provides a [Handler](https://godoc.org/github.com/buaazp/fasthttprouter#Router.Handler) interface to implement the fasthttp.ListenAndServe interface.
|
||||
|
||||
Just try it out for yourself, the usage of FastHttpRouter is very straightforward. The package is compact and minimalistic, but also probably one of the easiest routers to set up.
|
||||
|
||||
## Where can I find Middleware *X*?
|
||||
|
||||
This package just provides a very efficient request router with a few extra features. The router is just a [`fasthttp.RequestHandler`](https://godoc.org/github.com/valyala/fasthttp#RequestHandler), you can chain any `fasthttp.RequestHandler` compatible middleware before the router. Or you could [just write your own](https://justinas.org/writing-http-middleware-in-go/), it's very easy!
|
||||
|
||||
Have a look at these midware examples:
|
||||
|
||||
- [Auth Midware](examples/auth)
|
||||
- [Multi Hosts Midware](examples/hosts)
|
||||
|
||||
## Chaining with the NotFound handler
|
||||
|
||||
**NOTE: It might be required to set [Router.HandleMethodNotAllowed](http://godoc.org/github.com/buaazp/fasthttprouter#Router.HandleMethodNotAllowed) to `false` to avoid problems.**
|
||||
|
||||
You can use another [http.Handler](http://golang.org/pkg/net/http/#Handler), for example another router, to handle requests which could not be matched by this router by using the [Router.NotFound](http://godoc.org/github.com/buaazp/fasthttprouter#Router.NotFound) handler. This allows chaining.
|
||||
|
||||
### Static files
|
||||
The `NotFound` handler can for example be used to serve static files from the root path `/` (like an index.html file along with other assets):
|
||||
|
||||
```go
|
||||
// Serve static files from the ./public directory
|
||||
router.NotFound = fasthttp.FSHandler("./public", 0)
|
||||
```
|
||||
|
||||
But this approach sidesteps the strict core rules of this router to avoid routing problems. A cleaner approach is to use a distinct sub-path for serving files, like `/static/*filepath` or `/files/*filepath`.
|
||||
|
||||
## Web Frameworks based on FastHttpRouter
|
||||
|
||||
If the HttpRouter is a bit too minimalistic for you, you might try one of the following more high-level 3rd-party web frameworks building upon the HttpRouter package:
|
||||
|
||||
- Waiting for you to do this...
|
123
vendor/github.com/buaazp/fasthttprouter/path.go
generated
vendored
Normal file
123
vendor/github.com/buaazp/fasthttprouter/path.go
generated
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Based on the path package, Copyright 2009 The Go Authors.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package fasthttprouter
|
||||
|
||||
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||
// for p, eliminating . and .. elements.
|
||||
//
|
||||
// The following rules are applied iteratively until no further processing can
|
||||
// be done:
|
||||
// 1. Replace multiple slashes with a single slash.
|
||||
// 2. Eliminate each . path name element (the current directory).
|
||||
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||
// along with the non-.. element that precedes it.
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path.
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned
|
||||
func CleanPath(p string) string {
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
n := len(p)
|
||||
var buf []byte
|
||||
|
||||
// Invariants:
|
||||
// reading from path; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
|
||||
// path must start with '/'
|
||||
r := 1
|
||||
w := 1
|
||||
|
||||
if p[0] != '/' {
|
||||
r = 0
|
||||
buf = make([]byte, n+1)
|
||||
buf[0] = '/'
|
||||
}
|
||||
|
||||
trailing := n > 2 && p[n-1] == '/'
|
||||
|
||||
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||
// gets completely inlined (bufApp). So in contrast to the path package this
|
||||
// loop has no expensive function calls (except 1x make)
|
||||
|
||||
for r < n {
|
||||
switch {
|
||||
case p[r] == '/':
|
||||
// empty path element, trailing slash is added after the end
|
||||
r++
|
||||
|
||||
case p[r] == '.' && r+1 == n:
|
||||
trailing = true
|
||||
r++
|
||||
|
||||
case p[r] == '.' && p[r+1] == '/':
|
||||
// . element
|
||||
r++
|
||||
|
||||
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||
// .. element: remove to last /
|
||||
r += 2
|
||||
|
||||
if w > 1 {
|
||||
// can backtrack
|
||||
w--
|
||||
|
||||
if buf == nil {
|
||||
for w > 1 && p[w] != '/' {
|
||||
w--
|
||||
}
|
||||
} else {
|
||||
for w > 1 && buf[w] != '/' {
|
||||
w--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// real path element.
|
||||
// add slash if needed
|
||||
if w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// copy element
|
||||
for r < n && p[r] != '/' {
|
||||
bufApp(&buf, p, w, p[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// re-append trailing slash
|
||||
if trailing && w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
return p[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
// internal helper to lazily create a buffer if necessary
|
||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
if *buf == nil {
|
||||
if s[w] == c {
|
||||
return
|
||||
}
|
||||
|
||||
*buf = make([]byte, len(s))
|
||||
copy(*buf, s[:w])
|
||||
}
|
||||
(*buf)[w] = c
|
||||
}
|
374
vendor/github.com/buaazp/fasthttprouter/router.go
generated
vendored
Normal file
374
vendor/github.com/buaazp/fasthttprouter/router.go
generated
vendored
Normal file
@@ -0,0 +1,374 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
// Package fasthttprouter is a trie based high performance HTTP request router.
|
||||
//
|
||||
// A trivial example is:
|
||||
//
|
||||
// package main
|
||||
|
||||
// import (
|
||||
// "fmt"
|
||||
// "log"
|
||||
//
|
||||
// "github.com/buaazp/fasthttprouter"
|
||||
// "github.com/valyala/fasthttp"
|
||||
// )
|
||||
|
||||
// func Index(ctx *fasthttp.RequestCtx) {
|
||||
// fmt.Fprint(ctx, "Welcome!\n")
|
||||
// }
|
||||
|
||||
// func Hello(ctx *fasthttp.RequestCtx) {
|
||||
// fmt.Fprintf(ctx, "hello, %s!\n", ctx.UserValue("name"))
|
||||
// }
|
||||
|
||||
// func main() {
|
||||
// router := fasthttprouter.New()
|
||||
// router.GET("/", Index)
|
||||
// router.GET("/hello/:name", Hello)
|
||||
|
||||
// log.Fatal(fasthttp.ListenAndServe(":8080", router.Handler))
|
||||
// }
|
||||
//
|
||||
// The router matches incoming requests by the request method and the path.
|
||||
// If a handle is registered for this path and method, the router delegates the
|
||||
// request to that function.
|
||||
// For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to
|
||||
// register handles, for all other methods router.Handle can be used.
|
||||
//
|
||||
// The registered path, against which the router matches incoming requests, can
|
||||
// contain two types of parameters:
|
||||
// Syntax Type
|
||||
// :name named parameter
|
||||
// *name catch-all parameter
|
||||
//
|
||||
// Named parameters are dynamic path segments. They match anything until the
|
||||
// next '/' or the path end:
|
||||
// Path: /blog/:category/:post
|
||||
//
|
||||
// Requests:
|
||||
// /blog/go/request-routers match: category="go", post="request-routers"
|
||||
// /blog/go/request-routers/ no match, but the router would redirect
|
||||
// /blog/go/ no match
|
||||
// /blog/go/request-routers/comments no match
|
||||
//
|
||||
// Catch-all parameters match anything until the path end, including the
|
||||
// directory index (the '/' before the catch-all). Since they match anything
|
||||
// until the end, catch-all parameters must always be the final path element.
|
||||
// Path: /files/*filepath
|
||||
//
|
||||
// Requests:
|
||||
// /files/ match: filepath="/"
|
||||
// /files/LICENSE match: filepath="/LICENSE"
|
||||
// /files/templates/article.html match: filepath="/templates/article.html"
|
||||
// /files no match, but the router would redirect
|
||||
//
|
||||
// The value of parameters is inside ctx.UserValue
|
||||
// To retrieve the value of a parameter:
|
||||
// // use the name of the parameter
|
||||
// user := ps.UserValue("user")
|
||||
//
|
||||
|
||||
package fasthttprouter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultContentType = []byte("text/plain; charset=utf-8")
|
||||
questionMark = []byte("?")
|
||||
)
|
||||
|
||||
// Router is a http.Handler which can be used to dispatch requests to different
|
||||
// handler functions via configurable routes
|
||||
type Router struct {
|
||||
trees map[string]*node
|
||||
|
||||
// Enables automatic redirection if the current route can't be matched but a
|
||||
// handler for the path with (without) the trailing slash exists.
|
||||
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||
// client is redirected to /foo with http status code 301 for GET requests
|
||||
// and 307 for all other request methods.
|
||||
RedirectTrailingSlash bool
|
||||
|
||||
// If enabled, the router tries to fix the current request path, if no
|
||||
// handle is registered for it.
|
||||
// First superfluous path elements like ../ or // are removed.
|
||||
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
||||
// If a handle can be found for this route, the router makes a redirection
|
||||
// to the corrected path with status code 301 for GET requests and 307 for
|
||||
// all other request methods.
|
||||
// For example /FOO and /..//Foo could be redirected to /foo.
|
||||
// RedirectTrailingSlash is independent of this option.
|
||||
RedirectFixedPath bool
|
||||
|
||||
// If enabled, the router checks if another method is allowed for the
|
||||
// current route, if the current request can not be routed.
|
||||
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||
// and HTTP status code 405.
|
||||
// If no other Method is allowed, the request is delegated to the NotFound
|
||||
// handler.
|
||||
HandleMethodNotAllowed bool
|
||||
|
||||
// If enabled, the router automatically replies to OPTIONS requests.
|
||||
// Custom OPTIONS handlers take priority over automatic replies.
|
||||
HandleOPTIONS bool
|
||||
|
||||
// Configurable http.Handler which is called when no matching route is
|
||||
// found. If it is not set, http.NotFound is used.
|
||||
NotFound fasthttp.RequestHandler
|
||||
|
||||
// Configurable http.Handler which is called when a request
|
||||
// cannot be routed and HandleMethodNotAllowed is true.
|
||||
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
|
||||
// The "Allow" header with allowed request methods is set before the handler
|
||||
// is called.
|
||||
MethodNotAllowed fasthttp.RequestHandler
|
||||
|
||||
// Function to handle panics recovered from http handlers.
|
||||
// It should be used to generate a error page and return the http error code
|
||||
// 500 (Internal Server Error).
|
||||
// The handler can be used to keep your server from crashing because of
|
||||
// unrecovered panics.
|
||||
PanicHandler func(*fasthttp.RequestCtx, interface{})
|
||||
}
|
||||
|
||||
// New returns a new initialized Router.
|
||||
// Path auto-correction, including trailing slashes, is enabled by default.
|
||||
func New() *Router {
|
||||
return &Router{
|
||||
RedirectTrailingSlash: true,
|
||||
RedirectFixedPath: true,
|
||||
HandleMethodNotAllowed: true,
|
||||
HandleOPTIONS: true,
|
||||
}
|
||||
}
|
||||
|
||||
// GET is a shortcut for router.Handle("GET", path, handle)
|
||||
func (r *Router) GET(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("GET", path, handle)
|
||||
}
|
||||
|
||||
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
|
||||
func (r *Router) HEAD(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("HEAD", path, handle)
|
||||
}
|
||||
|
||||
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
|
||||
func (r *Router) OPTIONS(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("OPTIONS", path, handle)
|
||||
}
|
||||
|
||||
// POST is a shortcut for router.Handle("POST", path, handle)
|
||||
func (r *Router) POST(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("POST", path, handle)
|
||||
}
|
||||
|
||||
// PUT is a shortcut for router.Handle("PUT", path, handle)
|
||||
func (r *Router) PUT(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("PUT", path, handle)
|
||||
}
|
||||
|
||||
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
|
||||
func (r *Router) PATCH(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("PATCH", path, handle)
|
||||
}
|
||||
|
||||
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
|
||||
func (r *Router) DELETE(path string, handle fasthttp.RequestHandler) {
|
||||
r.Handle("DELETE", path, handle)
|
||||
}
|
||||
|
||||
// Handle registers a new request handle with the given path and method.
|
||||
//
|
||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||
// functions can be used.
|
||||
//
|
||||
// This function is intended for bulk loading and to allow the usage of less
|
||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||
// communication with a proxy).
|
||||
func (r *Router) Handle(method, path string, handle fasthttp.RequestHandler) {
|
||||
if path[0] != '/' {
|
||||
panic("path must begin with '/' in path '" + path + "'")
|
||||
}
|
||||
|
||||
if r.trees == nil {
|
||||
r.trees = make(map[string]*node)
|
||||
}
|
||||
|
||||
root := r.trees[method]
|
||||
if root == nil {
|
||||
root = new(node)
|
||||
r.trees[method] = root
|
||||
}
|
||||
|
||||
root.addRoute(path, handle)
|
||||
}
|
||||
|
||||
// ServeFiles serves files from the given file system root.
|
||||
// The path must end with "/*filepath", files are then served from the local
|
||||
// path /defined/root/dir/*filepath.
|
||||
// For example if root is "/etc" and *filepath is "passwd", the local file
|
||||
// "/etc/passwd" would be served.
|
||||
// Internally a http.FileServer is used, therefore http.NotFound is used instead
|
||||
// of the Router's NotFound handler.
|
||||
// router.ServeFiles("/src/*filepath", "/var/www")
|
||||
func (r *Router) ServeFiles(path string, rootPath string) {
|
||||
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
|
||||
panic("path must end with /*filepath in path '" + path + "'")
|
||||
}
|
||||
prefix := path[:len(path)-10]
|
||||
|
||||
fileHandler := fasthttp.FSHandler(rootPath, strings.Count(prefix, "/"))
|
||||
|
||||
r.GET(path, func(ctx *fasthttp.RequestCtx) {
|
||||
fileHandler(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) recv(ctx *fasthttp.RequestCtx) {
|
||||
if rcv := recover(); rcv != nil {
|
||||
r.PanicHandler(ctx, rcv)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup allows the manual lookup of a method + path combo.
|
||||
// This is e.g. useful to build a framework around this router.
|
||||
// If the path was found, it returns the handle function and the path parameter
|
||||
// values. Otherwise the third return value indicates whether a redirection to
|
||||
// the same path with an extra / without the trailing slash should be performed.
|
||||
func (r *Router) Lookup(method, path string, ctx *fasthttp.RequestCtx) (fasthttp.RequestHandler, bool) {
|
||||
if root := r.trees[method]; root != nil {
|
||||
return root.getValue(path, ctx)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Router) allowed(path, reqMethod string) (allow string) {
|
||||
if path == "*" || path == "/*" { // server-wide
|
||||
for method := range r.trees {
|
||||
if method == "OPTIONS" {
|
||||
continue
|
||||
}
|
||||
|
||||
// add request method to list of allowed methods
|
||||
if len(allow) == 0 {
|
||||
allow = method
|
||||
} else {
|
||||
allow += ", " + method
|
||||
}
|
||||
}
|
||||
} else { // specific path
|
||||
for method := range r.trees {
|
||||
// Skip the requested method - we already tried this one
|
||||
if method == reqMethod || method == "OPTIONS" {
|
||||
continue
|
||||
}
|
||||
|
||||
handle, _ := r.trees[method].getValue(path, nil)
|
||||
if handle != nil {
|
||||
// add request method to list of allowed methods
|
||||
if len(allow) == 0 {
|
||||
allow = method
|
||||
} else {
|
||||
allow += ", " + method
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(allow) > 0 {
|
||||
allow += ", OPTIONS"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handler makes the router implement the fasthttp.ListenAndServe interface.
|
||||
func (r *Router) Handler(ctx *fasthttp.RequestCtx) {
|
||||
if r.PanicHandler != nil {
|
||||
defer r.recv(ctx)
|
||||
}
|
||||
|
||||
path := string(ctx.Path())
|
||||
method := string(ctx.Method())
|
||||
if root := r.trees[method]; root != nil {
|
||||
if f, tsr := root.getValue(path, ctx); f != nil {
|
||||
f(ctx)
|
||||
return
|
||||
} else if method != "CONNECT" && path != "/" {
|
||||
code := 301 // Permanent redirect, request with GET method
|
||||
if method != "GET" {
|
||||
// Temporary redirect, request with same method
|
||||
// As of Go 1.3, Go does not support status code 308.
|
||||
code = 307
|
||||
}
|
||||
|
||||
if tsr && r.RedirectTrailingSlash {
|
||||
var uri string
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
uri = path[:len(path)-1]
|
||||
} else {
|
||||
uri = path + "/"
|
||||
}
|
||||
ctx.Redirect(uri, code)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to fix the request path
|
||||
if r.RedirectFixedPath {
|
||||
fixedPath, found := root.findCaseInsensitivePath(
|
||||
CleanPath(path),
|
||||
r.RedirectTrailingSlash,
|
||||
)
|
||||
|
||||
if found {
|
||||
queryBuf := ctx.URI().QueryString()
|
||||
if len(queryBuf) > 0 {
|
||||
fixedPath = append(fixedPath, questionMark...)
|
||||
fixedPath = append(fixedPath, queryBuf...)
|
||||
}
|
||||
uri := string(fixedPath)
|
||||
ctx.Redirect(uri, code)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if method == "OPTIONS" {
|
||||
// Handle OPTIONS requests
|
||||
if r.HandleOPTIONS {
|
||||
if allow := r.allowed(path, method); len(allow) > 0 {
|
||||
ctx.Response.Header.Set("Allow", allow)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle 405
|
||||
if r.HandleMethodNotAllowed {
|
||||
if allow := r.allowed(path, method); len(allow) > 0 {
|
||||
ctx.Response.Header.Set("Allow", allow)
|
||||
if r.MethodNotAllowed != nil {
|
||||
r.MethodNotAllowed(ctx)
|
||||
} else {
|
||||
ctx.SetStatusCode(fasthttp.StatusMethodNotAllowed)
|
||||
ctx.SetContentTypeBytes(defaultContentType)
|
||||
ctx.SetBodyString(fasthttp.StatusMessage(fasthttp.StatusMethodNotAllowed))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 404
|
||||
if r.NotFound != nil {
|
||||
r.NotFound(ctx)
|
||||
} else {
|
||||
ctx.Error(fasthttp.StatusMessage(fasthttp.StatusNotFound),
|
||||
fasthttp.StatusNotFound)
|
||||
}
|
||||
}
|
643
vendor/github.com/buaazp/fasthttprouter/tree.go
generated
vendored
Normal file
643
vendor/github.com/buaazp/fasthttprouter/tree.go
generated
vendored
Normal file
@@ -0,0 +1,643 @@
|
||||
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be found
|
||||
// in the LICENSE file.
|
||||
|
||||
package fasthttprouter
|
||||
|
||||
import (
|
||||
"github.com/valyala/fasthttp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func countParams(path string) uint8 {
|
||||
var n uint
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] != ':' && path[i] != '*' {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n >= 255 {
|
||||
return 255
|
||||
}
|
||||
return uint8(n)
|
||||
}
|
||||
|
||||
type nodeType uint8
|
||||
|
||||
const (
|
||||
static nodeType = iota // default
|
||||
root
|
||||
param
|
||||
catchAll
|
||||
)
|
||||
|
||||
type node struct {
|
||||
path string
|
||||
wildChild bool
|
||||
nType nodeType
|
||||
maxParams uint8
|
||||
indices string
|
||||
children []*node
|
||||
handle fasthttp.RequestHandler
|
||||
priority uint32
|
||||
}
|
||||
|
||||
// increments priority of the given child and reorders if necessary
|
||||
func (n *node) incrementChildPrio(pos int) int {
|
||||
n.children[pos].priority++
|
||||
prio := n.children[pos].priority
|
||||
|
||||
// adjust position (move to front)
|
||||
newPos := pos
|
||||
for newPos > 0 && n.children[newPos-1].priority < prio {
|
||||
// swap node positions
|
||||
tmpN := n.children[newPos-1]
|
||||
n.children[newPos-1] = n.children[newPos]
|
||||
n.children[newPos] = tmpN
|
||||
|
||||
newPos--
|
||||
}
|
||||
|
||||
// build new index char string
|
||||
if newPos != pos {
|
||||
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
|
||||
n.indices[pos:pos+1] + // the index char we move
|
||||
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
|
||||
}
|
||||
|
||||
return newPos
|
||||
}
|
||||
|
||||
// addRoute adds a node with the given handle to the path.
|
||||
// Not concurrency-safe!
|
||||
func (n *node) addRoute(path string, handle fasthttp.RequestHandler) {
|
||||
fullPath := path
|
||||
n.priority++
|
||||
numParams := countParams(path)
|
||||
|
||||
// non-empty tree
|
||||
if len(n.path) > 0 || len(n.children) > 0 {
|
||||
walk:
|
||||
for {
|
||||
// Update maxParams of the current node
|
||||
if numParams > n.maxParams {
|
||||
n.maxParams = numParams
|
||||
}
|
||||
|
||||
// Find the longest common prefix.
|
||||
// This also implies that the common prefix contains no ':' or '*'
|
||||
// since the existing key can't contain those chars.
|
||||
i := 0
|
||||
max := min(len(path), len(n.path))
|
||||
for i < max && path[i] == n.path[i] {
|
||||
i++
|
||||
}
|
||||
|
||||
// Split edge
|
||||
if i < len(n.path) {
|
||||
child := node{
|
||||
path: n.path[i:],
|
||||
wildChild: n.wildChild,
|
||||
nType: static,
|
||||
indices: n.indices,
|
||||
children: n.children,
|
||||
handle: n.handle,
|
||||
priority: n.priority - 1,
|
||||
}
|
||||
|
||||
// Update maxParams (max of all children)
|
||||
for i := range child.children {
|
||||
if child.children[i].maxParams > child.maxParams {
|
||||
child.maxParams = child.children[i].maxParams
|
||||
}
|
||||
}
|
||||
|
||||
n.children = []*node{&child}
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices = string([]byte{n.path[i]})
|
||||
n.path = path[:i]
|
||||
n.handle = nil
|
||||
n.wildChild = false
|
||||
}
|
||||
|
||||
// Make new node a child of this node
|
||||
if i < len(path) {
|
||||
path = path[i:]
|
||||
|
||||
if n.wildChild {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
|
||||
// Update maxParams of the child node
|
||||
if numParams > n.maxParams {
|
||||
n.maxParams = numParams
|
||||
}
|
||||
numParams--
|
||||
|
||||
// Check if the wildcard matches
|
||||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||
// Check for longer wildcard, e.g. :name and :names
|
||||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||
continue walk
|
||||
} else {
|
||||
// Wildcard conflict
|
||||
pathSeg := strings.SplitN(path, "/", 2)[0]
|
||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||
panic("'" + pathSeg +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing wildcard '" + n.path +
|
||||
"' in existing prefix '" + prefix +
|
||||
"'")
|
||||
}
|
||||
}
|
||||
|
||||
c := path[0]
|
||||
|
||||
// slash after param
|
||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
continue walk
|
||||
}
|
||||
|
||||
// Check if a child with the next path byte exists
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
i = n.incrementChildPrio(i)
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise insert it
|
||||
if c != ':' && c != '*' {
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices += string([]byte{c})
|
||||
child := &node{
|
||||
maxParams: numParams,
|
||||
}
|
||||
n.children = append(n.children, child)
|
||||
n.incrementChildPrio(len(n.indices) - 1)
|
||||
n = child
|
||||
}
|
||||
n.insertChild(numParams, path, fullPath, handle)
|
||||
return
|
||||
|
||||
} else if i == len(path) { // Make node a (in-path) leaf
|
||||
if n.handle != nil {
|
||||
panic("a handle is already registered for path '" + fullPath + "'")
|
||||
}
|
||||
n.handle = handle
|
||||
}
|
||||
return
|
||||
}
|
||||
} else { // Empty tree
|
||||
n.insertChild(numParams, path, fullPath, handle)
|
||||
n.nType = root
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) insertChild(numParams uint8, path, fullPath string, handle fasthttp.RequestHandler) {
|
||||
var offset int // already handled bytes of the path
|
||||
|
||||
// find prefix until first wildcard (beginning with ':'' or '*'')
|
||||
for i, max := 0, len(path); numParams > 0; i++ {
|
||||
c := path[i]
|
||||
if c != ':' && c != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
// find wildcard end (either '/' or path end)
|
||||
end := i + 1
|
||||
for end < max && path[end] != '/' {
|
||||
switch path[end] {
|
||||
// the wildcard name must not contain ':' and '*'
|
||||
case ':', '*':
|
||||
panic("only one wildcard per path segment is allowed, has: '" +
|
||||
path[i:] + "' in path '" + fullPath + "'")
|
||||
default:
|
||||
end++
|
||||
}
|
||||
}
|
||||
|
||||
// check if this Node existing children which would be
|
||||
// unreachable if we insert the wildcard here
|
||||
if len(n.children) > 0 {
|
||||
panic("wildcard route '" + path[i:end] +
|
||||
"' conflicts with existing children in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// check if the wildcard has a name
|
||||
if end-i < 2 {
|
||||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if c == ':' { // param
|
||||
// split path at the beginning of the wildcard
|
||||
if i > 0 {
|
||||
n.path = path[offset:i]
|
||||
offset = i
|
||||
}
|
||||
|
||||
child := &node{
|
||||
nType: param,
|
||||
maxParams: numParams,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.wildChild = true
|
||||
n = child
|
||||
n.priority++
|
||||
numParams--
|
||||
|
||||
// if the path doesn't end with the wildcard, then there
|
||||
// will be another non-wildcard subpath starting with '/'
|
||||
if end < max {
|
||||
n.path = path[offset:end]
|
||||
offset = end
|
||||
|
||||
child := &node{
|
||||
maxParams: numParams,
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n = child
|
||||
}
|
||||
|
||||
} else { // catchAll
|
||||
if end != max || numParams > 1 {
|
||||
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// currently fixed width 1 for '/'
|
||||
i--
|
||||
if path[i] != '/' {
|
||||
panic("no / before catch-all in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
n.path = path[offset:i]
|
||||
|
||||
// first node: catchAll node with empty path
|
||||
child := &node{
|
||||
wildChild: true,
|
||||
nType: catchAll,
|
||||
maxParams: 1,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.indices = string(path[i])
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// second node: node holding the variable
|
||||
child = &node{
|
||||
path: path[i:],
|
||||
nType: catchAll,
|
||||
maxParams: 1,
|
||||
handle: handle,
|
||||
priority: 1,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// insert remaining path part and handle to the leaf
|
||||
n.path = path[offset:]
|
||||
n.handle = handle
|
||||
}
|
||||
|
||||
// Returns the handle registered with the given path (key). The values of
|
||||
// wildcards are saved to a map.
|
||||
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||
// made if a handle exists with an extra (without the) trailing slash for the
|
||||
// given path.
|
||||
func (n *node) getValue(path string, ctx *fasthttp.RequestCtx) (handle fasthttp.RequestHandler, tsr bool) {
|
||||
walk: // outer loop for walking the tree
|
||||
for {
|
||||
if len(path) > len(n.path) {
|
||||
if path[:len(n.path)] == n.path {
|
||||
path = path[len(n.path):]
|
||||
// If this node does not have a wildcard (param or catchAll)
|
||||
// child, we can just look up the next child node and continue
|
||||
// to walk down the tree
|
||||
if !n.wildChild {
|
||||
c := path[0]
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
tsr = (path == "/" && n.handle != nil)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// handle wildcard child
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// handle calls to Router.allowed method with nil context
|
||||
if ctx != nil {
|
||||
ctx.SetUserValue(n.path[1:], path[:end])
|
||||
}
|
||||
|
||||
// we need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
path = path[end:]
|
||||
n = n.children[0]
|
||||
continue walk
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
tsr = (len(path) == end+1)
|
||||
return
|
||||
}
|
||||
|
||||
if handle = n.handle; handle != nil {
|
||||
return
|
||||
} else if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for TSR recommendation
|
||||
n = n.children[0]
|
||||
tsr = (n.path == "/" && n.handle != nil)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case catchAll:
|
||||
if ctx != nil {
|
||||
// save param value
|
||||
ctx.SetUserValue(n.path[2:], path)
|
||||
}
|
||||
handle = n.handle
|
||||
return
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
}
|
||||
} else if path == n.path {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if handle = n.handle; handle != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
tsr = (len(n.path) == 1 && n.handle != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
tsr = (path == "/") ||
|
||||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||
path == n.path[:len(n.path)-1] && n.handle != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
||||
// It can optionally also fix trailing slashes.
|
||||
// It returns the case-corrected path and a bool indicating whether the lookup
|
||||
// was successful.
|
||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||
return n.findCaseInsensitivePathRec(
|
||||
path,
|
||||
strings.ToLower(path),
|
||||
make([]byte, 0, len(path)+1), // preallocate enough memory for new path
|
||||
[4]byte{}, // empty rune buffer
|
||||
fixTrailingSlash,
|
||||
)
|
||||
}
|
||||
|
||||
// shift bytes in array by n bytes left
|
||||
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||||
switch n {
|
||||
case 0:
|
||||
return rb
|
||||
case 1:
|
||||
return [4]byte{rb[1], rb[2], rb[3], 0}
|
||||
case 2:
|
||||
return [4]byte{rb[2], rb[3]}
|
||||
case 3:
|
||||
return [4]byte{rb[3]}
|
||||
default:
|
||||
return [4]byte{}
|
||||
}
|
||||
}
|
||||
|
||||
// recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
||||
func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) {
|
||||
loNPath := strings.ToLower(n.path)
|
||||
|
||||
walk: // outer loop for walking the tree
|
||||
for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) {
|
||||
// add common path to result
|
||||
ciPath = append(ciPath, n.path...)
|
||||
|
||||
if path = path[len(n.path):]; len(path) > 0 {
|
||||
loOld := loPath
|
||||
loPath = loPath[len(loNPath):]
|
||||
|
||||
// If this node does not have a wildcard (param or catchAll) child,
|
||||
// we can just look up the next child node and continue to walk down
|
||||
// the tree
|
||||
if !n.wildChild {
|
||||
// skip rune bytes already processed
|
||||
rb = shiftNRuneBytes(rb, len(loNPath))
|
||||
|
||||
if rb[0] != 0 {
|
||||
// old rune not finished
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == rb[0] {
|
||||
// continue with child node
|
||||
n = n.children[i]
|
||||
loNPath = strings.ToLower(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// process a new rune
|
||||
var rv rune
|
||||
|
||||
// find rune start
|
||||
// runes are up to 4 byte long,
|
||||
// -4 would definitely be another rune
|
||||
var off int
|
||||
for max := min(len(loNPath), 3); off < max; off++ {
|
||||
if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) {
|
||||
// read rune from cached lowercase path
|
||||
rv, _ = utf8.DecodeRuneInString(loOld[i:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// calculate lowercase bytes of current rune
|
||||
utf8.EncodeRune(rb[:], rv)
|
||||
// skipp already processed bytes
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
// lowercase matches
|
||||
if n.indices[i] == rb[0] {
|
||||
// must use a recursive approach since both the
|
||||
// uppercase byte and the lowercase byte might exist
|
||||
// as an index
|
||||
if out, found := n.children[i].findCaseInsensitivePathRec(
|
||||
path, loPath, ciPath, rb, fixTrailingSlash,
|
||||
); found {
|
||||
return out, true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// same for uppercase rune, if it differs
|
||||
if up := unicode.ToUpper(rv); up != rv {
|
||||
utf8.EncodeRune(rb[:], up)
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
// uppercase matches
|
||||
if n.indices[i] == rb[0] {
|
||||
// continue with child node
|
||||
n = n.children[i]
|
||||
loNPath = strings.ToLower(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL
|
||||
// without a trailing slash if a leaf exists for that path
|
||||
return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil)
|
||||
}
|
||||
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// find param end (either '/' or path end)
|
||||
k := 0
|
||||
for k < len(path) && path[k] != '/' {
|
||||
k++
|
||||
}
|
||||
|
||||
// add param value to case insensitive path
|
||||
ciPath = append(ciPath, path[:k]...)
|
||||
|
||||
// we need to go deeper!
|
||||
if k < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
// continue with child node
|
||||
n = n.children[0]
|
||||
loNPath = strings.ToLower(n.path)
|
||||
loPath = loPath[k:]
|
||||
path = path[k:]
|
||||
continue
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
if fixTrailingSlash && len(path) == k+1 {
|
||||
return ciPath, true
|
||||
}
|
||||
return ciPath, false
|
||||
}
|
||||
|
||||
if n.handle != nil {
|
||||
return ciPath, true
|
||||
} else if fixTrailingSlash && len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists
|
||||
n = n.children[0]
|
||||
if n.path == "/" && n.handle != nil {
|
||||
return append(ciPath, '/'), true
|
||||
}
|
||||
}
|
||||
return ciPath, false
|
||||
|
||||
case catchAll:
|
||||
return append(ciPath, path...), true
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
} else {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if n.handle != nil {
|
||||
return ciPath, true
|
||||
}
|
||||
|
||||
// No handle found.
|
||||
// Try to fix the path by adding a trailing slash
|
||||
if fixTrailingSlash {
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
if (len(n.path) == 1 && n.handle != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handle != nil) {
|
||||
return append(ciPath, '/'), true
|
||||
}
|
||||
return ciPath, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return ciPath, false
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// Try to fix the path by adding / removing a trailing slash
|
||||
if fixTrailingSlash {
|
||||
if path == "/" {
|
||||
return ciPath, true
|
||||
}
|
||||
if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' &&
|
||||
loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil {
|
||||
return append(ciPath, n.path...), true
|
||||
}
|
||||
}
|
||||
return ciPath, false
|
||||
}
|
3
vendor/github.com/certifi/gocertifi/LICENSE
generated
vendored
Normal file
3
vendor/github.com/certifi/gocertifi/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
This Source Code Form is subject to the terms of the Mozilla Public License,
|
||||
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
|
||||
one at http://mozilla.org/MPL/2.0/.
|
60
vendor/github.com/certifi/gocertifi/README.md
generated
vendored
Normal file
60
vendor/github.com/certifi/gocertifi/README.md
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
# GoCertifi: SSL Certificates for Golang
|
||||
|
||||
This Go package contains a CA bundle that you can reference in your Go code.
|
||||
This is useful for systems that do not have CA bundles that Golang can find
|
||||
itself, or where a uniform set of CAs is valuable.
|
||||
|
||||
This is the same CA bundle that ships with the
|
||||
[Python Requests](https://github.com/kennethreitz/requests) library, and is a
|
||||
Golang specific port of [certifi](https://github.com/kennethreitz/certifi). The
|
||||
CA bundle is derived from Mozilla's canonical set.
|
||||
|
||||
## Usage
|
||||
|
||||
You can use the `gocertifi` package as follows:
|
||||
|
||||
```go
|
||||
import "github.com/certifi/gocertifi"
|
||||
|
||||
cert_pool, err := gocertifi.CACerts()
|
||||
```
|
||||
|
||||
You can use the returned `*x509.CertPool` as part of an HTTP transport, for example:
|
||||
|
||||
```go
|
||||
import (
|
||||
"net/http"
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
// Setup an HTTP client with a custom transport
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{RootCAs: cert_pool},
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
// Make an HTTP request using our custom transport
|
||||
resp, err := client.Get("https://example.com")
|
||||
```
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
Import as follows:
|
||||
|
||||
```go
|
||||
import "github.com/certifi/gocertifi"
|
||||
```
|
||||
|
||||
### Errors
|
||||
|
||||
```go
|
||||
var ErrParseFailed = errors.New("gocertifi: error when parsing certificates")
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```go
|
||||
func CACerts() (*x509.CertPool, error)
|
||||
```
|
||||
CACerts builds an X.509 certificate pool containing the Mozilla CA Certificate
|
||||
bundle. Returns nil on error along with an appropriate error code.
|
5298
vendor/github.com/certifi/gocertifi/certifi.go
generated
vendored
Normal file
5298
vendor/github.com/certifi/gocertifi/certifi.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
20
vendor/github.com/certifi/gocertifi/tasks.py
generated
vendored
Normal file
20
vendor/github.com/certifi/gocertifi/tasks.py
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
from invoke import task
|
||||
import requests
|
||||
|
||||
@task
|
||||
def update(ctx):
|
||||
r = requests.get('https://mkcert.org/generate/')
|
||||
r.raise_for_status()
|
||||
certs = r.content
|
||||
|
||||
with open('certifi.go', 'rb') as f:
|
||||
file = f.read()
|
||||
|
||||
file = file.split('`\n')
|
||||
assert len(file) == 3
|
||||
file[1] = certs
|
||||
|
||||
ctx.run("rm certifi.go")
|
||||
|
||||
with open('certifi.go', 'wb') as f:
|
||||
f.write('`\n'.join(file))
|
13
vendor/github.com/getsentry/raven-go/Dockerfile.test
generated
vendored
Normal file
13
vendor/github.com/getsentry/raven-go/Dockerfile.test
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.7
|
||||
|
||||
RUN mkdir -p /go/src/github.com/getsentry/raven-go
|
||||
WORKDIR /go/src/github.com/getsentry/raven-go
|
||||
ENV GOPATH /go
|
||||
|
||||
RUN go install -race std && go get golang.org/x/tools/cmd/cover
|
||||
|
||||
COPY . /go/src/github.com/getsentry/raven-go
|
||||
|
||||
RUN go get -v ./...
|
||||
|
||||
CMD ["./runtests.sh"]
|
28
vendor/github.com/getsentry/raven-go/LICENSE
generated
vendored
Normal file
28
vendor/github.com/getsentry/raven-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
|
||||
Copyright (c) 2015 Functional Software, Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Apollic Software, LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
13
vendor/github.com/getsentry/raven-go/README.md
generated
vendored
Normal file
13
vendor/github.com/getsentry/raven-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# raven [](https://travis-ci.org/getsentry/raven-go)
|
||||
|
||||
raven is a Go client for the [Sentry](https://github.com/getsentry/sentry)
|
||||
event/error logging system.
|
||||
|
||||
- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go)
|
||||
- [**Usage and Examples**](https://docs.sentry.io/clients/go/)
|
||||
|
||||
## Installation
|
||||
|
||||
```text
|
||||
go get github.com/getsentry/raven-go
|
||||
```
|
799
vendor/github.com/getsentry/raven-go/client.go
generated
vendored
Normal file
799
vendor/github.com/getsentry/raven-go/client.go
generated
vendored
Normal file
@@ -0,0 +1,799 @@
|
||||
// Package raven implements a client for the Sentry error logging service.
|
||||
package raven
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/certifi/gocertifi"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "raven-go/1.0"
|
||||
timestampFormat = `"2006-01-02T15:04:05.00"`
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPacketDropped = errors.New("raven: packet dropped")
|
||||
ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON")
|
||||
ErrMissingUser = errors.New("raven: dsn missing public key and/or password")
|
||||
ErrMissingPrivateKey = errors.New("raven: dsn missing private key")
|
||||
ErrMissingProjectID = errors.New("raven: dsn missing project id")
|
||||
)
|
||||
|
||||
type Severity string
|
||||
|
||||
// http://docs.python.org/2/howto/logging.html#logging-levels
|
||||
const (
|
||||
DEBUG = Severity("debug")
|
||||
INFO = Severity("info")
|
||||
WARNING = Severity("warning")
|
||||
ERROR = Severity("error")
|
||||
FATAL = Severity("fatal")
|
||||
)
|
||||
|
||||
type Timestamp time.Time
|
||||
|
||||
func (t Timestamp) MarshalJSON() ([]byte, error) {
|
||||
return []byte(time.Time(t).UTC().Format(timestampFormat)), nil
|
||||
}
|
||||
|
||||
func (timestamp *Timestamp) UnmarshalJSON(data []byte) error {
|
||||
t, err := time.Parse(timestampFormat, string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*timestamp = Timestamp(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
// An Interface is a Sentry interface that will be serialized as JSON.
|
||||
// It must implement json.Marshaler or use json struct tags.
|
||||
type Interface interface {
|
||||
// The Sentry class name. Example: sentry.interfaces.Stacktrace
|
||||
Class() string
|
||||
}
|
||||
|
||||
type Culpriter interface {
|
||||
Culprit() string
|
||||
}
|
||||
|
||||
type Transport interface {
|
||||
Send(url, authHeader string, packet *Packet) error
|
||||
}
|
||||
|
||||
type outgoingPacket struct {
|
||||
packet *Packet
|
||||
ch chan error
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Tags []Tag
|
||||
|
||||
func (tag *Tag) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal([2]string{tag.Key, tag.Value})
|
||||
}
|
||||
|
||||
func (t *Tag) UnmarshalJSON(data []byte) error {
|
||||
var tag [2]string
|
||||
if err := json.Unmarshal(data, &tag); err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Tag{tag[0], tag[1]}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tags) UnmarshalJSON(data []byte) error {
|
||||
var tags []Tag
|
||||
|
||||
switch data[0] {
|
||||
case '[':
|
||||
// Unmarshal into []Tag
|
||||
if err := json.Unmarshal(data, &tags); err != nil {
|
||||
return err
|
||||
}
|
||||
case '{':
|
||||
// Unmarshal into map[string]string
|
||||
tagMap := make(map[string]string)
|
||||
if err := json.Unmarshal(data, &tagMap); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert to []Tag
|
||||
for k, v := range tagMap {
|
||||
tags = append(tags, Tag{k, v})
|
||||
}
|
||||
default:
|
||||
return ErrUnableToUnmarshalJSON
|
||||
}
|
||||
|
||||
*t = tags
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/#building-the-json-packet
|
||||
type Packet struct {
|
||||
// Required
|
||||
Message string `json:"message"`
|
||||
|
||||
// Required, set automatically by Client.Send/Report via Packet.Init if blank
|
||||
EventID string `json:"event_id"`
|
||||
Project string `json:"project"`
|
||||
Timestamp Timestamp `json:"timestamp"`
|
||||
Level Severity `json:"level"`
|
||||
Logger string `json:"logger"`
|
||||
|
||||
// Optional
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Culprit string `json:"culprit,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
Release string `json:"release,omitempty"`
|
||||
Environment string `json:"environment,omitempty"`
|
||||
Tags Tags `json:"tags,omitempty"`
|
||||
Modules map[string]string `json:"modules,omitempty"`
|
||||
Fingerprint []string `json:"fingerprint,omitempty"`
|
||||
Extra map[string]interface{} `json:"extra,omitempty"`
|
||||
|
||||
Interfaces []Interface `json:"-"`
|
||||
}
|
||||
|
||||
// NewPacket constructs a packet with the specified message and interfaces.
|
||||
func NewPacket(message string, interfaces ...Interface) *Packet {
|
||||
extra := map[string]interface{}{
|
||||
"runtime.Version": runtime.Version(),
|
||||
"runtime.NumCPU": runtime.NumCPU(),
|
||||
"runtime.GOMAXPROCS": runtime.GOMAXPROCS(0), // 0 just returns the current value
|
||||
"runtime.NumGoroutine": runtime.NumGoroutine(),
|
||||
}
|
||||
return &Packet{
|
||||
Message: message,
|
||||
Interfaces: interfaces,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes required fields in a packet. It is typically called by
|
||||
// Client.Send/Report automatically.
|
||||
func (packet *Packet) Init(project string) error {
|
||||
if packet.Project == "" {
|
||||
packet.Project = project
|
||||
}
|
||||
if packet.EventID == "" {
|
||||
var err error
|
||||
packet.EventID, err = uuid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if time.Time(packet.Timestamp).IsZero() {
|
||||
packet.Timestamp = Timestamp(time.Now())
|
||||
}
|
||||
if packet.Level == "" {
|
||||
packet.Level = ERROR
|
||||
}
|
||||
if packet.Logger == "" {
|
||||
packet.Logger = "root"
|
||||
}
|
||||
if packet.ServerName == "" {
|
||||
packet.ServerName = hostname
|
||||
}
|
||||
if packet.Platform == "" {
|
||||
packet.Platform = "go"
|
||||
}
|
||||
|
||||
if packet.Culprit == "" {
|
||||
for _, inter := range packet.Interfaces {
|
||||
if c, ok := inter.(Culpriter); ok {
|
||||
packet.Culprit = c.Culprit()
|
||||
if packet.Culprit != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (packet *Packet) AddTags(tags map[string]string) {
|
||||
for k, v := range tags {
|
||||
packet.Tags = append(packet.Tags, Tag{k, v})
|
||||
}
|
||||
}
|
||||
|
||||
func uuid() (string, error) {
|
||||
id := make([]byte, 16)
|
||||
_, err := io.ReadFull(rand.Reader, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id[6] &= 0x0F // clear version
|
||||
id[6] |= 0x40 // set version to 4 (random uuid)
|
||||
id[8] &= 0x3F // clear variant
|
||||
id[8] |= 0x80 // set to IETF variant
|
||||
return hex.EncodeToString(id), nil
|
||||
}
|
||||
|
||||
func (packet *Packet) JSON() ([]byte, error) {
|
||||
packetJSON, err := json.Marshal(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
interfaces := make(map[string]Interface, len(packet.Interfaces))
|
||||
for _, inter := range packet.Interfaces {
|
||||
if inter != nil {
|
||||
interfaces[inter.Class()] = inter
|
||||
}
|
||||
}
|
||||
|
||||
if len(interfaces) > 0 {
|
||||
interfaceJSON, err := json.Marshal(interfaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packetJSON[len(packetJSON)-1] = ','
|
||||
packetJSON = append(packetJSON, interfaceJSON[1:]...)
|
||||
}
|
||||
|
||||
return packetJSON, nil
|
||||
}
|
||||
|
||||
type context struct {
|
||||
user *User
|
||||
http *Http
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
func (c *context) SetUser(u *User) { c.user = u }
|
||||
func (c *context) SetHttp(h *Http) { c.http = h }
|
||||
func (c *context) SetTags(t map[string]string) {
|
||||
if c.tags == nil {
|
||||
c.tags = make(map[string]string)
|
||||
}
|
||||
for k, v := range t {
|
||||
c.tags[k] = v
|
||||
}
|
||||
}
|
||||
func (c *context) Clear() {
|
||||
c.user = nil
|
||||
c.http = nil
|
||||
c.tags = nil
|
||||
}
|
||||
|
||||
// Return a list of interfaces to be used in appending with the rest
|
||||
func (c *context) interfaces() []Interface {
|
||||
len, i := 0, 0
|
||||
if c.user != nil {
|
||||
len++
|
||||
}
|
||||
if c.http != nil {
|
||||
len++
|
||||
}
|
||||
interfaces := make([]Interface, len)
|
||||
if c.user != nil {
|
||||
interfaces[i] = c.user
|
||||
i++
|
||||
}
|
||||
if c.http != nil {
|
||||
interfaces[i] = c.http
|
||||
i++
|
||||
}
|
||||
return interfaces
|
||||
}
|
||||
|
||||
// The maximum number of packets that will be buffered waiting to be delivered.
|
||||
// Packets will be dropped if the buffer is full. Used by NewClient.
|
||||
var MaxQueueBuffer = 100
|
||||
|
||||
func newTransport() Transport {
|
||||
t := &HTTPTransport{}
|
||||
rootCAs, err := gocertifi.CACerts()
|
||||
if err != nil {
|
||||
log.Println("raven: failed to load root TLS certificates:", err)
|
||||
} else {
|
||||
t.Client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{RootCAs: rootCAs},
|
||||
},
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func newClient(tags map[string]string) *Client {
|
||||
client := &Client{
|
||||
Transport: newTransport(),
|
||||
Tags: tags,
|
||||
context: &context{},
|
||||
queue: make(chan *outgoingPacket, MaxQueueBuffer),
|
||||
}
|
||||
client.SetDSN(os.Getenv("SENTRY_DSN"))
|
||||
return client
|
||||
}
|
||||
|
||||
// New constructs a new Sentry client instance
|
||||
func New(dsn string) (*Client, error) {
|
||||
client := newClient(nil)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// NewWithTags constructs a new Sentry client instance with default tags.
|
||||
func NewWithTags(dsn string, tags map[string]string) (*Client, error) {
|
||||
client := newClient(tags)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// NewClient constructs a Sentry client and spawns a background goroutine to
|
||||
// handle packets sent by Client.Report.
|
||||
//
|
||||
// Deprecated: use New and NewWithTags instead
|
||||
func NewClient(dsn string, tags map[string]string) (*Client, error) {
|
||||
client := newClient(tags)
|
||||
return client, client.SetDSN(dsn)
|
||||
}
|
||||
|
||||
// Client encapsulates a connection to a Sentry server. It must be initialized
|
||||
// by calling NewClient. Modification of fields concurrently with Send or after
|
||||
// calling Report for the first time is not thread-safe.
|
||||
type Client struct {
|
||||
Tags map[string]string
|
||||
|
||||
Transport Transport
|
||||
|
||||
// DropHandler is called when a packet is dropped because the buffer is full.
|
||||
DropHandler func(*Packet)
|
||||
|
||||
// Context that will get appending to all packets
|
||||
context *context
|
||||
|
||||
mu sync.RWMutex
|
||||
url string
|
||||
projectID string
|
||||
authHeader string
|
||||
release string
|
||||
environment string
|
||||
includePaths []string
|
||||
queue chan *outgoingPacket
|
||||
|
||||
// A WaitGroup to keep track of all currently in-progress captures
|
||||
// This is intended to be used with Client.Wait() to assure that
|
||||
// all messages have been transported before exiting the process.
|
||||
wg sync.WaitGroup
|
||||
|
||||
// A Once to track only starting up the background worker once
|
||||
start sync.Once
|
||||
}
|
||||
|
||||
// Initialize a default *Client instance
|
||||
var DefaultClient = newClient(nil)
|
||||
|
||||
// SetDSN updates a client with a new DSN. It safe to call after and
|
||||
// concurrently with calls to Report and Send.
|
||||
func (client *Client) SetDSN(dsn string) error {
|
||||
if dsn == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
uri, err := url.Parse(dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uri.User == nil {
|
||||
return ErrMissingUser
|
||||
}
|
||||
publicKey := uri.User.Username()
|
||||
secretKey, ok := uri.User.Password()
|
||||
if !ok {
|
||||
return ErrMissingPrivateKey
|
||||
}
|
||||
uri.User = nil
|
||||
|
||||
if idx := strings.LastIndex(uri.Path, "/"); idx != -1 {
|
||||
client.projectID = uri.Path[idx+1:]
|
||||
uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/"
|
||||
}
|
||||
if client.projectID == "" {
|
||||
return ErrMissingProjectID
|
||||
}
|
||||
|
||||
client.url = uri.String()
|
||||
|
||||
client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets the DSN for the default *Client instance
|
||||
func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) }
|
||||
|
||||
// SetRelease sets the "release" tag.
|
||||
func (client *Client) SetRelease(release string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
client.release = release
|
||||
}
|
||||
|
||||
// SetEnvironment sets the "environment" tag.
|
||||
func (client *Client) SetEnvironment(environment string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
client.environment = environment
|
||||
}
|
||||
|
||||
// SetRelease sets the "release" tag on the default *Client
|
||||
func SetRelease(release string) { DefaultClient.SetRelease(release) }
|
||||
|
||||
// SetEnvironment sets the "environment" tag on the default *Client
|
||||
func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) }
|
||||
|
||||
func (client *Client) worker() {
|
||||
for outgoingPacket := range client.queue {
|
||||
|
||||
client.mu.RLock()
|
||||
url, authHeader := client.url, client.authHeader
|
||||
client.mu.RUnlock()
|
||||
|
||||
outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet)
|
||||
client.wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// Capture asynchronously delivers a packet to the Sentry server. It is a no-op
|
||||
// when client is nil. A channel is provided if it is important to check for a
|
||||
// send's success.
|
||||
func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Keep track of all running Captures so that we can wait for them all to finish
|
||||
// *Must* call client.wg.Done() on any path that indicates that an event was
|
||||
// finished being acted upon, whether success or failure
|
||||
client.wg.Add(1)
|
||||
|
||||
ch = make(chan error, 1)
|
||||
|
||||
// Merge capture tags and client tags
|
||||
packet.AddTags(captureTags)
|
||||
packet.AddTags(client.Tags)
|
||||
packet.AddTags(client.context.tags)
|
||||
|
||||
// Initialize any required packet fields
|
||||
client.mu.RLock()
|
||||
projectID := client.projectID
|
||||
release := client.release
|
||||
environment := client.environment
|
||||
client.mu.RUnlock()
|
||||
|
||||
err := packet.Init(projectID)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
client.wg.Done()
|
||||
return
|
||||
}
|
||||
|
||||
packet.Release = release
|
||||
packet.Environment = environment
|
||||
|
||||
outgoingPacket := &outgoingPacket{packet, ch}
|
||||
|
||||
// Lazily start background worker until we
|
||||
// do our first write into the queue.
|
||||
client.start.Do(func() {
|
||||
go client.worker()
|
||||
})
|
||||
|
||||
select {
|
||||
case client.queue <- outgoingPacket:
|
||||
default:
|
||||
// Send would block, drop the packet
|
||||
if client.DropHandler != nil {
|
||||
client.DropHandler(packet)
|
||||
}
|
||||
ch <- ErrPacketDropped
|
||||
client.wg.Done()
|
||||
}
|
||||
|
||||
return packet.EventID, ch
|
||||
}
|
||||
|
||||
// Capture asynchronously delivers a packet to the Sentry server with the default *Client.
|
||||
// It is a no-op when client is nil. A channel is provided if it is important to check for a
|
||||
// send's success.
|
||||
func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) {
|
||||
return DefaultClient.Capture(packet, captureTags)
|
||||
}
|
||||
|
||||
// CaptureMessage formats and delivers a string message to the Sentry server.
|
||||
func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
|
||||
eventID, _ := client.Capture(packet, tags)
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureMessage formats and delivers a string message to the Sentry server with the default *Client
|
||||
func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureMessage(message, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
|
||||
func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...)
|
||||
eventID, ch := client.Capture(packet, tags)
|
||||
<-ch
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent.
|
||||
func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureErrors formats and delivers an error to the Sentry server.
|
||||
// Adds a stacktrace to the packet, excluding the call to this method.
|
||||
func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...)
|
||||
eventID, _ := client.Capture(packet, tags)
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureErrors formats and delivers an error to the Sentry server using the default *Client.
|
||||
// Adds a stacktrace to the packet, excluding the call to this method.
|
||||
func CaptureError(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureError(err, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
if client == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...)
|
||||
eventID, ch := client.Capture(packet, tags)
|
||||
<-ch
|
||||
|
||||
return eventID
|
||||
}
|
||||
|
||||
// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string {
|
||||
return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
|
||||
// If an error is captured, both the error and the reported Sentry error ID are returned.
|
||||
func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
|
||||
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
|
||||
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
|
||||
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
|
||||
// be completely noop though if we cared.
|
||||
defer func() {
|
||||
var packet *Packet
|
||||
err = recover()
|
||||
switch rval := err.(type) {
|
||||
case nil:
|
||||
return
|
||||
case error:
|
||||
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
|
||||
default:
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
|
||||
}
|
||||
|
||||
errorID, _ = client.Capture(packet, tags)
|
||||
}()
|
||||
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs.
|
||||
// If an error is captured, both the error and the reported Sentry error ID are returned.
|
||||
func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
|
||||
return DefaultClient.CapturePanic(f, tags, interfaces...)
|
||||
}
|
||||
|
||||
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func (client *Client) CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) {
|
||||
// Note: This doesn't need to check for client, because we still want to go through the defer/recover path
|
||||
// Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the
|
||||
// *Packet just to be thrown away, this should not be the normal case. Could be refactored to
|
||||
// be completely noop though if we cared.
|
||||
defer func() {
|
||||
var packet *Packet
|
||||
err = recover()
|
||||
switch rval := err.(type) {
|
||||
case nil:
|
||||
return
|
||||
case error:
|
||||
packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...)
|
||||
default:
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...)
|
||||
}
|
||||
|
||||
var ch chan error
|
||||
errorID, ch = client.Capture(packet, tags)
|
||||
<-ch
|
||||
}()
|
||||
|
||||
f()
|
||||
return
|
||||
}
|
||||
|
||||
// CapturePanicAndWait is identical to CaptureError, except it blocks and assures that the event was sent
|
||||
func CapturePanicAndWait(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) {
|
||||
return DefaultClient.CapturePanicAndWait(f, tags, interfaces...)
|
||||
}
|
||||
|
||||
func (client *Client) Close() {
|
||||
close(client.queue)
|
||||
}
|
||||
|
||||
func Close() { DefaultClient.Close() }
|
||||
|
||||
// Wait blocks and waits for all events to finish being sent to Sentry server
|
||||
func (client *Client) Wait() {
|
||||
client.wg.Wait()
|
||||
}
|
||||
|
||||
// Wait blocks and waits for all events to finish being sent to Sentry server
|
||||
func Wait() { DefaultClient.Wait() }
|
||||
|
||||
func (client *Client) URL() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.url
|
||||
}
|
||||
|
||||
func URL() string { return DefaultClient.URL() }
|
||||
|
||||
func (client *Client) ProjectID() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.projectID
|
||||
}
|
||||
|
||||
func ProjectID() string { return DefaultClient.ProjectID() }
|
||||
|
||||
func (client *Client) Release() string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.release
|
||||
}
|
||||
|
||||
func Release() string { return DefaultClient.Release() }
|
||||
|
||||
func IncludePaths() []string { return DefaultClient.IncludePaths() }
|
||||
|
||||
func (client *Client) IncludePaths() []string {
|
||||
client.mu.RLock()
|
||||
defer client.mu.RUnlock()
|
||||
|
||||
return client.includePaths
|
||||
}
|
||||
|
||||
func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) }
|
||||
|
||||
func (client *Client) SetIncludePaths(p []string) {
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
|
||||
client.includePaths = p
|
||||
}
|
||||
|
||||
func (c *Client) SetUserContext(u *User) { c.context.SetUser(u) }
|
||||
func (c *Client) SetHttpContext(h *Http) { c.context.SetHttp(h) }
|
||||
func (c *Client) SetTagsContext(t map[string]string) { c.context.SetTags(t) }
|
||||
func (c *Client) ClearContext() { c.context.Clear() }
|
||||
|
||||
func SetUserContext(u *User) { DefaultClient.SetUserContext(u) }
|
||||
func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) }
|
||||
func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) }
|
||||
func ClearContext() { DefaultClient.ClearContext() }
|
||||
|
||||
// HTTPTransport is the default transport, delivering packets to Sentry via the
|
||||
// HTTP API.
|
||||
type HTTPTransport struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error {
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, contentType, err := serializedPacket(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializing packet: %v", err)
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't create new request: %v", err)
|
||||
}
|
||||
req.Header.Set("X-Sentry-Auth", authHeader)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
res, err := t.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("raven: got http status %d", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializedPacket(packet *Packet) (io.Reader, string, error) {
|
||||
packetJSON, err := packet.JSON()
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error marshaling packet %+v to JSON: %v", packet, err)
|
||||
}
|
||||
|
||||
// Only deflate/base64 the packet if it is bigger than 1KB, as there is
|
||||
// overhead.
|
||||
if len(packetJSON) > 1000 {
|
||||
buf := &bytes.Buffer{}
|
||||
b64 := base64.NewEncoder(base64.StdEncoding, buf)
|
||||
deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression)
|
||||
deflate.Write(packetJSON)
|
||||
deflate.Close()
|
||||
b64.Close()
|
||||
return buf, "application/octet-stream", nil
|
||||
}
|
||||
return bytes.NewReader(packetJSON), "application/json", nil
|
||||
}
|
||||
|
||||
var hostname string
|
||||
|
||||
func init() {
|
||||
hostname, _ = os.Hostname()
|
||||
}
|
41
vendor/github.com/getsentry/raven-go/exception.go
generated
vendored
Normal file
41
vendor/github.com/getsentry/raven-go/exception.go
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
package raven
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`)
|
||||
|
||||
func NewException(err error, stacktrace *Stacktrace) *Exception {
|
||||
msg := err.Error()
|
||||
ex := &Exception{
|
||||
Stacktrace: stacktrace,
|
||||
Value: msg,
|
||||
Type: reflect.TypeOf(err).String(),
|
||||
}
|
||||
if m := errorMsgPattern.FindStringSubmatch(msg); m != nil {
|
||||
ex.Module, ex.Value = m[1], m[2]
|
||||
}
|
||||
return ex
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
|
||||
type Exception struct {
|
||||
// Required
|
||||
Value string `json:"value"`
|
||||
|
||||
// Optional
|
||||
Type string `json:"type,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Exception) Class() string { return "exception" }
|
||||
|
||||
func (e *Exception) Culprit() string {
|
||||
if e.Stacktrace == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Stacktrace.Culprit()
|
||||
}
|
84
vendor/github.com/getsentry/raven-go/http.go
generated
vendored
Normal file
84
vendor/github.com/getsentry/raven-go/http.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
package raven
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewHttp(req *http.Request) *Http {
|
||||
proto := "http"
|
||||
if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
proto = "https"
|
||||
}
|
||||
h := &Http{
|
||||
Method: req.Method,
|
||||
Cookies: req.Header.Get("Cookie"),
|
||||
Query: sanitizeQuery(req.URL.Query()).Encode(),
|
||||
URL: proto + "://" + req.Host + req.URL.Path,
|
||||
Headers: make(map[string]string, len(req.Header)),
|
||||
}
|
||||
if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||
}
|
||||
for k, v := range req.Header {
|
||||
h.Headers[k] = strings.Join(v, ",")
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
var querySecretFields = []string{"password", "passphrase", "passwd", "secret"}
|
||||
|
||||
func sanitizeQuery(query url.Values) url.Values {
|
||||
for _, keyword := range querySecretFields {
|
||||
for field := range query {
|
||||
if strings.Contains(field, keyword) {
|
||||
query[field] = []string{"********"}
|
||||
}
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type Http struct {
|
||||
// Required
|
||||
URL string `json:"url"`
|
||||
Method string `json:"method"`
|
||||
Query string `json:"query_string,omitempty"`
|
||||
|
||||
// Optional
|
||||
Cookies string `json:"cookies,omitempty"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Must be either a string or map[string]string
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (h *Http) Class() string { return "request" }
|
||||
|
||||
// Recovery handler to wrap the stdlib net/http Mux.
|
||||
// Example:
|
||||
// http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) {
|
||||
// ...
|
||||
// }))
|
||||
func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rval := recover(); rval != nil {
|
||||
debug.PrintStack()
|
||||
rvalStr := fmt.Sprint(rval)
|
||||
packet := NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r))
|
||||
Capture(packet, nil)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
49
vendor/github.com/getsentry/raven-go/interfaces.go
generated
vendored
Normal file
49
vendor/github.com/getsentry/raven-go/interfaces.go
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
package raven
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#message-interface
|
||||
type Message struct {
|
||||
// Required
|
||||
Message string `json:"message"`
|
||||
|
||||
// Optional
|
||||
Params []interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Message) Class() string { return "logentry" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#template-interface
|
||||
type Template struct {
|
||||
// Required
|
||||
Filename string `json:"filename"`
|
||||
Lineno int `json:"lineno"`
|
||||
ContextLine string `json:"context_line"`
|
||||
|
||||
// Optional
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
AbsolutePath string `json:"abs_path,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Template) Class() string { return "template" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type User struct {
|
||||
// All fields are optional
|
||||
ID string `json:"id,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
IP string `json:"ip_address,omitempty"`
|
||||
}
|
||||
|
||||
func (h *User) Class() string { return "user" }
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces
|
||||
type Query struct {
|
||||
// Required
|
||||
Query string `json:"query"`
|
||||
|
||||
// Optional
|
||||
Engine string `json:"engine,omitempty"`
|
||||
}
|
||||
|
||||
func (q *Query) Class() string { return "query" }
|
4
vendor/github.com/getsentry/raven-go/runtests.sh
generated
vendored
Executable file
4
vendor/github.com/getsentry/raven-go/runtests.sh
generated
vendored
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
go test -race ./...
|
||||
go test -cover ./...
|
||||
go test -v ./...
|
213
vendor/github.com/getsentry/raven-go/stacktrace.go
generated
vendored
Normal file
213
vendor/github.com/getsentry/raven-go/stacktrace.go
generated
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
// Some code from the runtime/debug package of the Go standard library.
|
||||
|
||||
package raven
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"go/build"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces
|
||||
type Stacktrace struct {
|
||||
// Required
|
||||
Frames []*StacktraceFrame `json:"frames"`
|
||||
}
|
||||
|
||||
func (s *Stacktrace) Class() string { return "stacktrace" }
|
||||
|
||||
func (s *Stacktrace) Culprit() string {
|
||||
for i := len(s.Frames) - 1; i >= 0; i-- {
|
||||
frame := s.Frames[i]
|
||||
if frame.InApp == true && frame.Module != "" && frame.Function != "" {
|
||||
return frame.Module + "." + frame.Function
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type StacktraceFrame struct {
|
||||
// At least one required
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Function string `json:"function,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
|
||||
// Optional
|
||||
Lineno int `json:"lineno,omitempty"`
|
||||
Colno int `json:"colno,omitempty"`
|
||||
AbsolutePath string `json:"abs_path,omitempty"`
|
||||
ContextLine string `json:"context_line,omitempty"`
|
||||
PreContext []string `json:"pre_context,omitempty"`
|
||||
PostContext []string `json:"post_context,omitempty"`
|
||||
InApp bool `json:"in_app"`
|
||||
}
|
||||
|
||||
// Intialize and populate a new stacktrace, skipping skip frames.
|
||||
//
|
||||
// context is the number of surrounding lines that should be included for context.
|
||||
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
|
||||
// one line with no surrounding context, and 0 returns no context.
|
||||
//
|
||||
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
||||
// be considered "in app".
|
||||
func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace {
|
||||
var frames []*StacktraceFrame
|
||||
for i := 1 + skip; ; i++ {
|
||||
pc, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
|
||||
if frame != nil {
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
}
|
||||
// If there are no frames, the entire stacktrace is nil
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
// Optimize the path where there's only 1 frame
|
||||
if len(frames) == 1 {
|
||||
return &Stacktrace{frames}
|
||||
}
|
||||
// Sentry wants the frames with the oldest first, so reverse them
|
||||
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
||||
frames[i], frames[j] = frames[j], frames[i]
|
||||
}
|
||||
return &Stacktrace{frames}
|
||||
}
|
||||
|
||||
// Build a single frame using data returned from runtime.Caller.
|
||||
//
|
||||
// context is the number of surrounding lines that should be included for context.
|
||||
// Setting context to 3 would try to get seven lines. Setting context to -1 returns
|
||||
// one line with no surrounding context, and 0 returns no context.
|
||||
//
|
||||
// appPackagePrefixes is a list of prefixes used to check whether a package should
|
||||
// be considered "in app".
|
||||
func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame {
|
||||
frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false}
|
||||
frame.Module, frame.Function = functionName(pc)
|
||||
|
||||
// `runtime.goexit` is effectively a placeholder that comes from
|
||||
// runtime/asm_amd64.s and is meaningless.
|
||||
if frame.Module == "runtime" && frame.Function == "goexit" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if frame.Module == "main" {
|
||||
frame.InApp = true
|
||||
} else {
|
||||
for _, prefix := range appPackagePrefixes {
|
||||
if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") {
|
||||
frame.InApp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if context > 0 {
|
||||
contextLines, lineIdx := fileContext(file, line, context)
|
||||
if len(contextLines) > 0 {
|
||||
for i, line := range contextLines {
|
||||
switch {
|
||||
case i < lineIdx:
|
||||
frame.PreContext = append(frame.PreContext, string(line))
|
||||
case i == lineIdx:
|
||||
frame.ContextLine = string(line)
|
||||
default:
|
||||
frame.PostContext = append(frame.PostContext, string(line))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if context == -1 {
|
||||
contextLine, _ := fileContext(file, line, 0)
|
||||
if len(contextLine) > 0 {
|
||||
frame.ContextLine = string(contextLine[0])
|
||||
}
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
// Retrieve the name of the package and function containing the PC.
|
||||
func functionName(pc uintptr) (pack string, name string) {
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
name = fn.Name()
|
||||
// We get this:
|
||||
// runtime/debug.*T·ptrmethod
|
||||
// and want this:
|
||||
// pack = runtime/debug
|
||||
// name = *T.ptrmethod
|
||||
if idx := strings.LastIndex(name, "."); idx != -1 {
|
||||
pack = name[:idx]
|
||||
name = name[idx+1:]
|
||||
}
|
||||
name = strings.Replace(name, "·", ".", -1)
|
||||
return
|
||||
}
|
||||
|
||||
var fileCacheLock sync.Mutex
|
||||
var fileCache = make(map[string][][]byte)
|
||||
|
||||
func fileContext(filename string, line, context int) ([][]byte, int) {
|
||||
fileCacheLock.Lock()
|
||||
defer fileCacheLock.Unlock()
|
||||
lines, ok := fileCache[filename]
|
||||
if !ok {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
fileCache[filename] = lines
|
||||
}
|
||||
line-- // stack trace lines are 1-indexed
|
||||
start := line - context
|
||||
var idx int
|
||||
if start < 0 {
|
||||
start = 0
|
||||
idx = line
|
||||
} else {
|
||||
idx = context
|
||||
}
|
||||
end := line + context + 1
|
||||
if line >= len(lines) {
|
||||
return nil, 0
|
||||
}
|
||||
if end > len(lines) {
|
||||
end = len(lines)
|
||||
}
|
||||
return lines[start:end], idx
|
||||
}
|
||||
|
||||
var trimPaths []string
|
||||
|
||||
// Try to trim the GOROOT or GOPATH prefix off of a filename
|
||||
func trimPath(filename string) string {
|
||||
for _, prefix := range trimPaths {
|
||||
if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Collect all source directories, and make sure they
|
||||
// end in a trailing "separator"
|
||||
for _, prefix := range build.Default.SrcDirs() {
|
||||
if prefix[len(prefix)-1] != filepath.Separator {
|
||||
prefix += string(filepath.Separator)
|
||||
}
|
||||
trimPaths = append(trimPaths, prefix)
|
||||
}
|
||||
}
|
20
vendor/github.com/getsentry/raven-go/writer.go
generated
vendored
Normal file
20
vendor/github.com/getsentry/raven-go/writer.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
package raven
|
||||
|
||||
type Writer struct {
|
||||
Client *Client
|
||||
Level Severity
|
||||
Logger string // Logger name reported to Sentry
|
||||
}
|
||||
|
||||
// Write formats the byte slice p into a string, and sends a message to
|
||||
// Sentry at the severity level indicated by the Writer w.
|
||||
func (w *Writer) Write(p []byte) (int, error) {
|
||||
message := string(p)
|
||||
|
||||
packet := NewPacket(message, &Message{message, nil})
|
||||
packet.Level = w.Level
|
||||
packet.Logger = w.Logger
|
||||
w.Client.Capture(packet, nil)
|
||||
|
||||
return len(p), nil
|
||||
}
|
57
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
57
vendor/github.com/go-sql-driver/mysql/AUTHORS
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
|
||||
|
||||
# If you are submitting a patch, please add your name or the name of the
|
||||
# organization which holds the copyright to this list in alphabetical order.
|
||||
|
||||
# Names should be added to this file as
|
||||
# Name <email address>
|
||||
# The email address is not required for organizations.
|
||||
# Please keep the list sorted.
|
||||
|
||||
|
||||
# Individual Persons
|
||||
|
||||
Aaron Hopkins <go-sql-driver at die.net>
|
||||
Arne Hormann <arnehormann at gmail.com>
|
||||
Carlos Nieto <jose.carlos at menteslibres.net>
|
||||
Chris Moos <chris at tech9computers.com>
|
||||
Daniel Nichter <nil at codenode.com>
|
||||
Daniël van Eeden <git at myname.nl>
|
||||
DisposaBoy <disposaboy at dby.me>
|
||||
Egor Smolyakov <egorsmkv at gmail.com>
|
||||
Frederick Mayle <frederickmayle at gmail.com>
|
||||
Gustavo Kristic <gkristic at gmail.com>
|
||||
Hanno Braun <mail at hannobraun.com>
|
||||
Henri Yandell <flamefew at gmail.com>
|
||||
Hirotaka Yamamoto <ymmt2005 at gmail.com>
|
||||
INADA Naoki <songofacandy at gmail.com>
|
||||
James Harr <james.harr at gmail.com>
|
||||
Jian Zhen <zhenjl at gmail.com>
|
||||
Joshua Prunier <joshua.prunier at gmail.com>
|
||||
Julien Lefevre <julien.lefevr at gmail.com>
|
||||
Julien Schmidt <go-sql-driver at julienschmidt.com>
|
||||
Kamil Dziedzic <kamil at klecza.pl>
|
||||
Kevin Malachowski <kevin at chowski.com>
|
||||
Lennart Rudolph <lrudolph at hmc.edu>
|
||||
Leonardo YongUk Kim <dalinaum at gmail.com>
|
||||
Luca Looz <luca.looz92 at gmail.com>
|
||||
Lucas Liu <extrafliu at gmail.com>
|
||||
Luke Scott <luke at webconnex.com>
|
||||
Michael Woolnough <michael.woolnough at gmail.com>
|
||||
Nicola Peduzzi <thenikso at gmail.com>
|
||||
Olivier Mengué <dolmen at cpan.org>
|
||||
Paul Bonser <misterpib at gmail.com>
|
||||
Runrioter Wung <runrioter at gmail.com>
|
||||
Soroush Pour <me at soroushjp.com>
|
||||
Stan Putrya <root.vagner at gmail.com>
|
||||
Stanley Gunawan <gunawan.stanley at gmail.com>
|
||||
Xiangyu Hu <xiangyu.hu at outlook.com>
|
||||
Xiaobing Jiang <s7v7nislands at gmail.com>
|
||||
Xiuming Chen <cc at cxm.cc>
|
||||
Zhenye Xie <xiezhenye at gmail.com>
|
||||
|
||||
# Organizations
|
||||
|
||||
Barracuda Networks, Inc.
|
||||
Google Inc.
|
||||
Stripe Inc.
|
119
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
Normal file
119
vendor/github.com/go-sql-driver/mysql/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
## Version 1.3 (2016-12-01)
|
||||
|
||||
Changes:
|
||||
|
||||
- Go 1.1 is no longer supported
|
||||
- Use decimals fields in MySQL to format time types (#249)
|
||||
- Buffer optimizations (#269)
|
||||
- TLS ServerName defaults to the host (#283)
|
||||
- Refactoring (#400, #410, #437)
|
||||
- Adjusted documentation for second generation CloudSQL (#485)
|
||||
- Documented DSN system var quoting rules (#502)
|
||||
- Made statement.Close() calls idempotent to avoid errors in Go 1.6+ (#512)
|
||||
|
||||
New Features:
|
||||
|
||||
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
|
||||
- Support for returning table alias on Columns() (#289, #359, #382)
|
||||
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318, #490)
|
||||
- Support for uint64 parameters with high bit set (#332, #345)
|
||||
- Cleartext authentication plugin support (#327)
|
||||
- Exported ParseDSN function and the Config struct (#403, #419, #429)
|
||||
- Read / Write timeouts (#401)
|
||||
- Support for JSON field type (#414)
|
||||
- Support for multi-statements and multi-results (#411, #431)
|
||||
- DSN parameter to set the driver-side max_allowed_packet value manually (#489)
|
||||
- Native password authentication plugin support (#494, #524)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fixed handling of queries without columns and rows (#255)
|
||||
- Fixed a panic when SetKeepAlive() failed (#298)
|
||||
- Handle ERR packets while reading rows (#321)
|
||||
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
|
||||
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
|
||||
- Actually zero out bytes in handshake response (#378)
|
||||
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
|
||||
- Fixed tests with MySQL 5.7.9+ (#380)
|
||||
- QueryUnescape TLS config names (#397)
|
||||
- Fixed "broken pipe" error by writing to closed socket (#390)
|
||||
- Fixed LOAD LOCAL DATA INFILE buffering (#424)
|
||||
- Fixed parsing of floats into float64 when placeholders are used (#434)
|
||||
- Fixed DSN tests with Go 1.7+ (#459)
|
||||
- Handle ERR packets while waiting for EOF (#473)
|
||||
- Invalidate connection on error while discarding additional results (#513)
|
||||
- Allow terminating packets of length 0 (#516)
|
||||
|
||||
|
||||
## Version 1.2 (2014-06-03)
|
||||
|
||||
Changes:
|
||||
|
||||
- We switched back to a "rolling release". `go get` installs the current master branch again
|
||||
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
|
||||
- Exported errors to allow easy checking from application code
|
||||
- Enabled TCP Keepalives on TCP connections
|
||||
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
|
||||
- The DSN parser also checks for a missing separating slash
|
||||
- Faster binary date / datetime to string formatting
|
||||
- Also exported the MySQLWarning type
|
||||
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
|
||||
- writePacket() automatically writes the packet size to the header
|
||||
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
|
||||
|
||||
New Features:
|
||||
|
||||
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
|
||||
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
|
||||
- Logging of critical errors is configurable with `SetLogger`
|
||||
- Google CloudSQL support
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Allow more than 32 parameters in prepared statements
|
||||
- Various old_password fixes
|
||||
- Fixed TestConcurrent test to pass Go's race detection
|
||||
- Fixed appendLengthEncodedInteger for large numbers
|
||||
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
|
||||
|
||||
|
||||
## Version 1.1 (2013-11-02)
|
||||
|
||||
Changes:
|
||||
|
||||
- Go-MySQL-Driver now requires Go 1.1
|
||||
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
|
||||
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
|
||||
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
|
||||
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
|
||||
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
|
||||
- Optimized the buffer for reading
|
||||
- stmt.Query now caches column metadata
|
||||
- New Logo
|
||||
- Changed the copyright header to include all contributors
|
||||
- Improved the LOAD INFILE documentation
|
||||
- The driver struct is now exported to make the driver directly accessible
|
||||
- Refactored the driver tests
|
||||
- Added more benchmarks and moved all to a separate file
|
||||
- Other small refactoring
|
||||
|
||||
New Features:
|
||||
|
||||
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
|
||||
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
|
||||
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
|
||||
- Convert to DB timezone when inserting `time.Time`
|
||||
- Splitted packets (more than 16MB) are now merged correctly
|
||||
- Fixed false positive `io.EOF` errors when the data was fully read
|
||||
- Avoid panics on reuse of closed connections
|
||||
- Fixed empty string producing false nil values
|
||||
- Fixed sign byte for positive TIME fields
|
||||
|
||||
|
||||
## Version 1.0 (2013-05-14)
|
||||
|
||||
Initial Release
|
23
vendor/github.com/go-sql-driver/mysql/CONTRIBUTING.md
generated
vendored
Normal file
23
vendor/github.com/go-sql-driver/mysql/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file.
|
||||
Don't forget to add yourself to the AUTHORS file.
|
||||
|
||||
### Code Review
|
||||
|
||||
Everyone is invited to review and comment on pull requests.
|
||||
If it looks fine to you, comment with "LGTM" (Looks good to me).
|
||||
|
||||
If changes are required, notice the reviewers with "PTAL" (Please take another look) after committing the fixes.
|
||||
|
||||
Before merging the Pull Request, at least one [team member](https://github.com/go-sql-driver?tab=members) must have commented with "LGTM".
|
||||
|
||||
## Development Ideas
|
||||
|
||||
If you are looking for ideas for code contributions, please check our [Development Ideas](https://github.com/go-sql-driver/mysql/wiki/Development-Ideas) Wiki page.
|
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
Normal file
373
vendor/github.com/go-sql-driver/mysql/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
443
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
Normal file
443
vendor/github.com/go-sql-driver/mysql/README.md
generated
vendored
Normal file
@@ -0,0 +1,443 @@
|
||||
# Go-MySQL-Driver
|
||||
|
||||
A MySQL-Driver for Go's [database/sql](https://golang.org/pkg/database/sql/) package
|
||||
|
||||

|
||||
|
||||
---------------------------------------
|
||||
* [Features](#features)
|
||||
* [Requirements](#requirements)
|
||||
* [Installation](#installation)
|
||||
* [Usage](#usage)
|
||||
* [DSN (Data Source Name)](#dsn-data-source-name)
|
||||
* [Password](#password)
|
||||
* [Protocol](#protocol)
|
||||
* [Address](#address)
|
||||
* [Parameters](#parameters)
|
||||
* [Examples](#examples)
|
||||
* [LOAD DATA LOCAL INFILE support](#load-data-local-infile-support)
|
||||
* [time.Time support](#timetime-support)
|
||||
* [Unicode support](#unicode-support)
|
||||
* [Testing / Development](#testing--development)
|
||||
* [License](#license)
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## Features
|
||||
* Lightweight and [fast](https://github.com/go-sql-driver/sql-benchmark "golang MySQL-Driver performance")
|
||||
* Native Go implementation. No C-bindings, just pure Go
|
||||
* Connections over TCP/IPv4, TCP/IPv6, Unix domain sockets or [custom protocols](https://godoc.org/github.com/go-sql-driver/mysql#DialFunc)
|
||||
* Automatic handling of broken connections
|
||||
* Automatic Connection Pooling *(by database/sql package)*
|
||||
* Supports queries larger than 16MB
|
||||
* Full [`sql.RawBytes`](https://golang.org/pkg/database/sql/#RawBytes) support.
|
||||
* Intelligent `LONG DATA` handling in prepared statements
|
||||
* Secure `LOAD DATA LOCAL INFILE` support with file Whitelisting and `io.Reader` support
|
||||
* Optional `time.Time` parsing
|
||||
* Optional placeholder interpolation
|
||||
|
||||
## Requirements
|
||||
* Go 1.2 or higher
|
||||
* MySQL (4.1+), MariaDB, Percona Server, Google CloudSQL or Sphinx (2.2.3+)
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## Installation
|
||||
Simple install the package to your [$GOPATH](https://github.com/golang/go/wiki/GOPATH "GOPATH") with the [go tool](https://golang.org/cmd/go/ "go command") from shell:
|
||||
```bash
|
||||
$ go get github.com/go-sql-driver/mysql
|
||||
```
|
||||
Make sure [Git is installed](https://git-scm.com/downloads) on your machine and in your system's `PATH`.
|
||||
|
||||
## Usage
|
||||
_Go MySQL Driver_ is an implementation of Go's `database/sql/driver` interface. You only need to import the driver and can use the full [`database/sql`](https://golang.org/pkg/database/sql/) API then.
|
||||
|
||||
Use `mysql` as `driverName` and a valid [DSN](#dsn-data-source-name) as `dataSourceName`:
|
||||
```go
|
||||
import "database/sql"
|
||||
import _ "github.com/go-sql-driver/mysql"
|
||||
|
||||
db, err := sql.Open("mysql", "user:password@/dbname")
|
||||
```
|
||||
|
||||
[Examples are available in our Wiki](https://github.com/go-sql-driver/mysql/wiki/Examples "Go-MySQL-Driver Examples").
|
||||
|
||||
|
||||
### DSN (Data Source Name)
|
||||
|
||||
The Data Source Name has a common format, like e.g. [PEAR DB](http://pear.php.net/manual/en/package.database.db.intro-dsn.php) uses it, but without type-prefix (optional parts marked by squared brackets):
|
||||
```
|
||||
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
|
||||
```
|
||||
|
||||
A DSN in its fullest form:
|
||||
```
|
||||
username:password@protocol(address)/dbname?param=value
|
||||
```
|
||||
|
||||
Except for the databasename, all values are optional. So the minimal DSN is:
|
||||
```
|
||||
/dbname
|
||||
```
|
||||
|
||||
If you do not want to preselect a database, leave `dbname` empty:
|
||||
```
|
||||
/
|
||||
```
|
||||
This has the same effect as an empty DSN string:
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
Alternatively, [Config.FormatDSN](https://godoc.org/github.com/go-sql-driver/mysql#Config.FormatDSN) can be used to create a DSN string by filling a struct.
|
||||
|
||||
#### Password
|
||||
Passwords can consist of any character. Escaping is **not** necessary.
|
||||
|
||||
#### Protocol
|
||||
See [net.Dial](https://golang.org/pkg/net/#Dial) for more information which networks are available.
|
||||
In general you should use an Unix domain socket if available and TCP otherwise for best performance.
|
||||
|
||||
#### Address
|
||||
For TCP and UDP networks, addresses have the form `host:port`.
|
||||
If `host` is a literal IPv6 address, it must be enclosed in square brackets.
|
||||
The functions [net.JoinHostPort](https://golang.org/pkg/net/#JoinHostPort) and [net.SplitHostPort](https://golang.org/pkg/net/#SplitHostPort) manipulate addresses in this form.
|
||||
|
||||
For Unix domain sockets the address is the absolute path to the MySQL-Server-socket, e.g. `/var/run/mysqld/mysqld.sock` or `/tmp/mysql.sock`.
|
||||
|
||||
#### Parameters
|
||||
*Parameters are case-sensitive!*
|
||||
|
||||
Notice that any of `true`, `TRUE`, `True` or `1` is accepted to stand for a true boolean value. Not surprisingly, false can be specified as any of: `false`, `FALSE`, `False` or `0`.
|
||||
|
||||
##### `allowAllFiles`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`allowAllFiles=true` disables the file Whitelist for `LOAD DATA LOCAL INFILE` and allows *all* files.
|
||||
[*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)
|
||||
|
||||
##### `allowCleartextPasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`allowCleartextPasswords=true` allows using the [cleartext client side plugin](http://dev.mysql.com/doc/en/cleartext-authentication-plugin.html) if required by an account, such as one defined with the [PAM authentication plugin](http://dev.mysql.com/doc/en/pam-authentication-plugin.html). Sending passwords in clear text may be a security problem in some configurations. To avoid problems if there is any possibility that the password would be intercepted, clients should connect to MySQL Server using a method that protects the password. Possibilities include [TLS / SSL](#tls), IPsec, or a private network.
|
||||
|
||||
##### `allowNativePasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
`allowNativePasswords=true` allows the usage of the mysql native password method.
|
||||
|
||||
##### `allowOldPasswords`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
`allowOldPasswords=true` allows the usage of the insecure old password method. This should be avoided, but is necessary in some cases. See also [the old_passwords wiki page](https://github.com/go-sql-driver/mysql/wiki/old_passwords).
|
||||
|
||||
##### `charset`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <name>
|
||||
Default: none
|
||||
```
|
||||
|
||||
Sets the charset used for client-server interaction (`"SET NAMES <value>"`). If multiple charsets are set (separated by a comma), the following charset is used if setting the charset failes. This enables for example support for `utf8mb4` ([introduced in MySQL 5.5.3](http://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html)) with fallback to `utf8` for older servers (`charset=utf8mb4,utf8`).
|
||||
|
||||
Usage of the `charset` parameter is discouraged because it issues additional queries to the server.
|
||||
Unless you need the fallback behavior, please use `collation` instead.
|
||||
|
||||
##### `collation`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <name>
|
||||
Default: utf8_general_ci
|
||||
```
|
||||
|
||||
Sets the collation used for client-server interaction on connection. In contrast to `charset`, `collation` does not issue additional queries. If the specified collation is unavailable on the target server, the connection will fail.
|
||||
|
||||
A list of valid charsets for a server is retrievable with `SHOW COLLATION`.
|
||||
|
||||
##### `clientFoundRows`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`clientFoundRows=true` causes an UPDATE to return the number of matching rows instead of the number of rows changed.
|
||||
|
||||
##### `columnsWithAlias`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
When `columnsWithAlias` is true, calls to `sql.Rows.Columns()` will return the table alias and the column name separated by a dot. For example:
|
||||
|
||||
```
|
||||
SELECT u.id FROM users as u
|
||||
```
|
||||
|
||||
will return `u.id` instead of just `id` if `columnsWithAlias=true`.
|
||||
|
||||
##### `interpolateParams`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
If `interpolateParams` is true, placeholders (`?`) in calls to `db.Query()` and `db.Exec()` are interpolated into a single query string with given parameters. This reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with `interpolateParams=false`.
|
||||
|
||||
*This can not be used together with the multibyte encodings BIG5, CP932, GB2312, GBK or SJIS. These are blacklisted as they may [introduce a SQL injection vulnerability](http://stackoverflow.com/a/12118602/3430118)!*
|
||||
|
||||
##### `loc`
|
||||
|
||||
```
|
||||
Type: string
|
||||
Valid Values: <escaped name>
|
||||
Default: UTC
|
||||
```
|
||||
|
||||
Sets the location for time.Time values (when using `parseTime=true`). *"Local"* sets the system's location. See [time.LoadLocation](https://golang.org/pkg/time/#LoadLocation) for details.
|
||||
|
||||
Note that this sets the location for time.Time values but does not change MySQL's [time_zone setting](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html). For that see the [time_zone system variable](#system-variables), which can also be set as a DSN parameter.
|
||||
|
||||
Please keep in mind, that param values must be [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)'ed. Alternatively you can manually replace the `/` with `%2F`. For example `US/Pacific` would be `loc=US%2FPacific`.
|
||||
|
||||
##### `maxAllowedPacket`
|
||||
```
|
||||
Type: decimal number
|
||||
Default: 0
|
||||
```
|
||||
|
||||
Max packet size allowed in bytes. Use `maxAllowedPacket=0` to automatically fetch the `max_allowed_packet` variable from server.
|
||||
|
||||
##### `multiStatements`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
Allow multiple statements in one query. While this allows batch queries, it also greatly increases the risk of SQL injections. Only the result of the first query is returned, all other results are silently discarded.
|
||||
|
||||
When `multiStatements` is used, `?` parameters must only be used in the first statement.
|
||||
|
||||
##### `parseTime`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`parseTime=true` changes the output type of `DATE` and `DATETIME` values to `time.Time` instead of `[]byte` / `string`
|
||||
|
||||
|
||||
##### `readTimeout`
|
||||
|
||||
```
|
||||
Type: decimal number
|
||||
Default: 0
|
||||
```
|
||||
|
||||
I/O read timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||
|
||||
##### `strict`
|
||||
|
||||
```
|
||||
Type: bool
|
||||
Valid Values: true, false
|
||||
Default: false
|
||||
```
|
||||
|
||||
`strict=true` enables a driver-side strict mode in which MySQL warnings are treated as errors. This mode should not be used in production as it may lead to data corruption in certain situations.
|
||||
|
||||
A server-side strict mode, which is safe for production use, can be set via the [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) system variable.
|
||||
|
||||
By default MySQL also treats notes as warnings. Use [`sql_notes=false`](http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_sql_notes) to ignore notes.
|
||||
|
||||
##### `timeout`
|
||||
|
||||
```
|
||||
Type: decimal number
|
||||
Default: OS default
|
||||
```
|
||||
|
||||
*Driver* side connection timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*. To set a server side timeout, use the parameter [`wait_timeout`](http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_wait_timeout).
|
||||
|
||||
##### `tls`
|
||||
|
||||
```
|
||||
Type: bool / string
|
||||
Valid Values: true, false, skip-verify, <name>
|
||||
Default: false
|
||||
```
|
||||
|
||||
`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side). Use a custom value registered with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig).
|
||||
|
||||
##### `writeTimeout`
|
||||
|
||||
```
|
||||
Type: decimal number
|
||||
Default: 0
|
||||
```
|
||||
|
||||
I/O write timeout. The value must be a decimal number with an unit suffix ( *"ms"*, *"s"*, *"m"*, *"h"* ), such as *"30s"*, *"0.5m"* or *"1m30s"*.
|
||||
|
||||
|
||||
##### System Variables
|
||||
|
||||
Any other parameters are interpreted as system variables:
|
||||
* `<boolean_var>=<value>`: `SET <boolean_var>=<value>`
|
||||
* `<enum_var>=<value>`: `SET <enum_var>=<value>`
|
||||
* `<string_var>=%27<value>%27`: `SET <string_var>='<value>'`
|
||||
|
||||
Rules:
|
||||
* The values for string variables must be quoted with '
|
||||
* The values must also be [url.QueryEscape](http://golang.org/pkg/net/url/#QueryEscape)'ed!
|
||||
(which implies values of string variables must be wrapped with `%27`)
|
||||
|
||||
Examples:
|
||||
* `autocommit=1`: `SET autocommit=1`
|
||||
* [`time_zone=%27Europe%2FParis%27`](https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html): `SET time_zone='Europe/Paris'`
|
||||
* [`tx_isolation=%27REPEATABLE-READ%27`](https://dev.mysql.com/doc/refman/5.5/en/server-system-variables.html#sysvar_tx_isolation): `SET tx_isolation='REPEATABLE-READ'`
|
||||
|
||||
|
||||
#### Examples
|
||||
```
|
||||
user@unix(/path/to/socket)/dbname
|
||||
```
|
||||
|
||||
```
|
||||
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
|
||||
```
|
||||
|
||||
```
|
||||
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
|
||||
```
|
||||
|
||||
Treat warnings as errors by setting the system variable [`sql_mode`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html):
|
||||
```
|
||||
user:password@/dbname?sql_mode=TRADITIONAL
|
||||
```
|
||||
|
||||
TCP via IPv6:
|
||||
```
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
|
||||
```
|
||||
|
||||
TCP on a remote host, e.g. Amazon RDS:
|
||||
```
|
||||
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
|
||||
```
|
||||
|
||||
Google Cloud SQL on App Engine (First Generation MySQL Server):
|
||||
```
|
||||
user@cloudsql(project-id:instance-name)/dbname
|
||||
```
|
||||
|
||||
Google Cloud SQL on App Engine (Second Generation MySQL Server):
|
||||
```
|
||||
user@cloudsql(project-id:regionname:instance-name)/dbname
|
||||
```
|
||||
|
||||
TCP using default port (3306) on localhost:
|
||||
```
|
||||
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
|
||||
```
|
||||
|
||||
Use the default protocol (tcp) and host (localhost:3306):
|
||||
```
|
||||
user:password@/dbname
|
||||
```
|
||||
|
||||
No Database preselected:
|
||||
```
|
||||
user:password@/
|
||||
```
|
||||
|
||||
### `LOAD DATA LOCAL INFILE` support
|
||||
For this feature you need direct access to the package. Therefore you must change the import path (no `_`):
|
||||
```go
|
||||
import "github.com/go-sql-driver/mysql"
|
||||
```
|
||||
|
||||
Files must be whitelisted by registering them with `mysql.RegisterLocalFile(filepath)` (recommended) or the Whitelist check must be deactivated by using the DSN parameter `allowAllFiles=true` ([*Might be insecure!*](http://dev.mysql.com/doc/refman/5.7/en/load-data-local.html)).
|
||||
|
||||
To use a `io.Reader` a handler function must be registered with `mysql.RegisterReaderHandler(name, handler)` which returns a `io.Reader` or `io.ReadCloser`. The Reader is available with the filepath `Reader::<name>` then. Choose different names for different handlers and `DeregisterReaderHandler` when you don't need it anymore.
|
||||
|
||||
See the [godoc of Go-MySQL-Driver](https://godoc.org/github.com/go-sql-driver/mysql "golang mysql driver documentation") for details.
|
||||
|
||||
|
||||
### `time.Time` support
|
||||
The default internal output type of MySQL `DATE` and `DATETIME` values is `[]byte` which allows you to scan the value into a `[]byte`, `string` or `sql.RawBytes` variable in your programm.
|
||||
|
||||
However, many want to scan MySQL `DATE` and `DATETIME` values into `time.Time` variables, which is the logical opposite in Go to `DATE` and `DATETIME` in MySQL. You can do that by changing the internal output type from `[]byte` to `time.Time` with the DSN parameter `parseTime=true`. You can set the default [`time.Time` location](https://golang.org/pkg/time/#Location) with the `loc` DSN parameter.
|
||||
|
||||
**Caution:** As of Go 1.1, this makes `time.Time` the only variable type you can scan `DATE` and `DATETIME` values into. This breaks for example [`sql.RawBytes` support](https://github.com/go-sql-driver/mysql/wiki/Examples#rawbytes).
|
||||
|
||||
Alternatively you can use the [`NullTime`](https://godoc.org/github.com/go-sql-driver/mysql#NullTime) type as the scan destination, which works with both `time.Time` and `string` / `[]byte`.
|
||||
|
||||
|
||||
### Unicode support
|
||||
Since version 1.1 Go-MySQL-Driver automatically uses the collation `utf8_general_ci` by default.
|
||||
|
||||
Other collations / charsets can be set using the [`collation`](#collation) DSN parameter.
|
||||
|
||||
Version 1.0 of the driver recommended adding `&charset=utf8` (alias for `SET NAMES utf8`) to the DSN to enable proper UTF-8 support. This is not necessary anymore. The [`collation`](#collation) parameter should be preferred to set another collation / charset than the default.
|
||||
|
||||
See http://dev.mysql.com/doc/refman/5.7/en/charset-unicode.html for more details on MySQL's Unicode support.
|
||||
|
||||
|
||||
## Testing / Development
|
||||
To run the driver tests you may need to adjust the configuration. See the [Testing Wiki-Page](https://github.com/go-sql-driver/mysql/wiki/Testing "Testing") for details.
|
||||
|
||||
Go-MySQL-Driver is not feature-complete yet. Your help is very appreciated.
|
||||
If you want to contribute, you can work on an [open issue](https://github.com/go-sql-driver/mysql/issues?state=open) or review a [pull request](https://github.com/go-sql-driver/mysql/pulls).
|
||||
|
||||
See the [Contribution Guidelines](https://github.com/go-sql-driver/mysql/blob/master/CONTRIBUTING.md) for details.
|
||||
|
||||
---------------------------------------
|
||||
|
||||
## License
|
||||
Go-MySQL-Driver is licensed under the [Mozilla Public License Version 2.0](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||
|
||||
Mozilla summarizes the license scope as follows:
|
||||
> MPL: The copyleft applies to any files containing MPLed code.
|
||||
|
||||
|
||||
That means:
|
||||
* You can **use** the **unchanged** source code both in private and commercially
|
||||
* When distributing, you **must publish** the source code of any **changed files** licensed under the MPL 2.0 under a) the MPL 2.0 itself or b) a compatible license (e.g. GPL 3.0 or Apache License 2.0)
|
||||
* You **needn't publish** the source code of your library as long as the files licensed under the MPL 2.0 are **unchanged**
|
||||
|
||||
Please read the [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you have further questions regarding the license.
|
||||
|
||||
You can read the full terms here: [LICENSE](https://raw.github.com/go-sql-driver/mysql/master/LICENSE)
|
||||
|
||||

|
||||
|
19
vendor/github.com/go-sql-driver/mysql/appengine.go
generated
vendored
Normal file
19
vendor/github.com/go-sql-driver/mysql/appengine.go
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build appengine
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"appengine/cloudsql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterDial("cloudsql", cloudsql.Dial)
|
||||
}
|
147
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
Normal file
147
vendor/github.com/go-sql-driver/mysql/buffer.go
generated
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBufSize = 4096
|
||||
|
||||
// A buffer which is used for both reading and writing.
|
||||
// This is possible since communication on each connection is synchronous.
|
||||
// In other words, we can't write and read simultaneously on the same connection.
|
||||
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
|
||||
// Also highly optimized for this particular use case.
|
||||
type buffer struct {
|
||||
buf []byte
|
||||
nc net.Conn
|
||||
idx int
|
||||
length int
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func newBuffer(nc net.Conn) buffer {
|
||||
var b [defaultBufSize]byte
|
||||
return buffer{
|
||||
buf: b[:],
|
||||
nc: nc,
|
||||
}
|
||||
}
|
||||
|
||||
// fill reads into the buffer until at least _need_ bytes are in it
|
||||
func (b *buffer) fill(need int) error {
|
||||
n := b.length
|
||||
|
||||
// move existing data to the beginning
|
||||
if n > 0 && b.idx > 0 {
|
||||
copy(b.buf[0:n], b.buf[b.idx:])
|
||||
}
|
||||
|
||||
// grow buffer if necessary
|
||||
// TODO: let the buffer shrink again at some point
|
||||
// Maybe keep the org buf slice and swap back?
|
||||
if need > len(b.buf) {
|
||||
// Round up to the next multiple of the default size
|
||||
newBuf := make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
|
||||
copy(newBuf, b.buf)
|
||||
b.buf = newBuf
|
||||
}
|
||||
|
||||
b.idx = 0
|
||||
|
||||
for {
|
||||
if b.timeout > 0 {
|
||||
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nn, err := b.nc.Read(b.buf[n:])
|
||||
n += nn
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
if n < need {
|
||||
continue
|
||||
}
|
||||
b.length = n
|
||||
return nil
|
||||
|
||||
case io.EOF:
|
||||
if n >= need {
|
||||
b.length = n
|
||||
return nil
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns next N bytes from buffer.
|
||||
// The returned slice is only guaranteed to be valid until the next read
|
||||
func (b *buffer) readNext(need int) ([]byte, error) {
|
||||
if b.length < need {
|
||||
// refill
|
||||
if err := b.fill(need); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
offset := b.idx
|
||||
b.idx += need
|
||||
b.length -= need
|
||||
return b.buf[offset:b.idx], nil
|
||||
}
|
||||
|
||||
// returns a buffer with the requested size.
|
||||
// If possible, a slice from the existing buffer is returned.
|
||||
// Otherwise a bigger buffer is made.
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeBuffer(length int) []byte {
|
||||
if b.length > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// test (cheap) general case first
|
||||
if length <= defaultBufSize || length <= cap(b.buf) {
|
||||
return b.buf[:length]
|
||||
}
|
||||
|
||||
if length < maxPacketSize {
|
||||
b.buf = make([]byte, length)
|
||||
return b.buf
|
||||
}
|
||||
return make([]byte, length)
|
||||
}
|
||||
|
||||
// shortcut which can be used if the requested buffer is guaranteed to be
|
||||
// smaller than defaultBufSize
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeSmallBuffer(length int) []byte {
|
||||
if b.length == 0 {
|
||||
return b.buf[:length]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// takeCompleteBuffer returns the complete existing buffer.
|
||||
// This can be used if the necessary buffer size is unknown.
|
||||
// Only one buffer (total) can be used at a time.
|
||||
func (b *buffer) takeCompleteBuffer() []byte {
|
||||
if b.length == 0 {
|
||||
return b.buf
|
||||
}
|
||||
return nil
|
||||
}
|
250
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
Normal file
250
vendor/github.com/go-sql-driver/mysql/collations.go
generated
vendored
Normal file
@@ -0,0 +1,250 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
const defaultCollation = "utf8_general_ci"
|
||||
|
||||
// A list of available collations mapped to the internal ID.
|
||||
// To update this map use the following MySQL query:
|
||||
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS
|
||||
var collations = map[string]byte{
|
||||
"big5_chinese_ci": 1,
|
||||
"latin2_czech_cs": 2,
|
||||
"dec8_swedish_ci": 3,
|
||||
"cp850_general_ci": 4,
|
||||
"latin1_german1_ci": 5,
|
||||
"hp8_english_ci": 6,
|
||||
"koi8r_general_ci": 7,
|
||||
"latin1_swedish_ci": 8,
|
||||
"latin2_general_ci": 9,
|
||||
"swe7_swedish_ci": 10,
|
||||
"ascii_general_ci": 11,
|
||||
"ujis_japanese_ci": 12,
|
||||
"sjis_japanese_ci": 13,
|
||||
"cp1251_bulgarian_ci": 14,
|
||||
"latin1_danish_ci": 15,
|
||||
"hebrew_general_ci": 16,
|
||||
"tis620_thai_ci": 18,
|
||||
"euckr_korean_ci": 19,
|
||||
"latin7_estonian_cs": 20,
|
||||
"latin2_hungarian_ci": 21,
|
||||
"koi8u_general_ci": 22,
|
||||
"cp1251_ukrainian_ci": 23,
|
||||
"gb2312_chinese_ci": 24,
|
||||
"greek_general_ci": 25,
|
||||
"cp1250_general_ci": 26,
|
||||
"latin2_croatian_ci": 27,
|
||||
"gbk_chinese_ci": 28,
|
||||
"cp1257_lithuanian_ci": 29,
|
||||
"latin5_turkish_ci": 30,
|
||||
"latin1_german2_ci": 31,
|
||||
"armscii8_general_ci": 32,
|
||||
"utf8_general_ci": 33,
|
||||
"cp1250_czech_cs": 34,
|
||||
"ucs2_general_ci": 35,
|
||||
"cp866_general_ci": 36,
|
||||
"keybcs2_general_ci": 37,
|
||||
"macce_general_ci": 38,
|
||||
"macroman_general_ci": 39,
|
||||
"cp852_general_ci": 40,
|
||||
"latin7_general_ci": 41,
|
||||
"latin7_general_cs": 42,
|
||||
"macce_bin": 43,
|
||||
"cp1250_croatian_ci": 44,
|
||||
"utf8mb4_general_ci": 45,
|
||||
"utf8mb4_bin": 46,
|
||||
"latin1_bin": 47,
|
||||
"latin1_general_ci": 48,
|
||||
"latin1_general_cs": 49,
|
||||
"cp1251_bin": 50,
|
||||
"cp1251_general_ci": 51,
|
||||
"cp1251_general_cs": 52,
|
||||
"macroman_bin": 53,
|
||||
"utf16_general_ci": 54,
|
||||
"utf16_bin": 55,
|
||||
"utf16le_general_ci": 56,
|
||||
"cp1256_general_ci": 57,
|
||||
"cp1257_bin": 58,
|
||||
"cp1257_general_ci": 59,
|
||||
"utf32_general_ci": 60,
|
||||
"utf32_bin": 61,
|
||||
"utf16le_bin": 62,
|
||||
"binary": 63,
|
||||
"armscii8_bin": 64,
|
||||
"ascii_bin": 65,
|
||||
"cp1250_bin": 66,
|
||||
"cp1256_bin": 67,
|
||||
"cp866_bin": 68,
|
||||
"dec8_bin": 69,
|
||||
"greek_bin": 70,
|
||||
"hebrew_bin": 71,
|
||||
"hp8_bin": 72,
|
||||
"keybcs2_bin": 73,
|
||||
"koi8r_bin": 74,
|
||||
"koi8u_bin": 75,
|
||||
"latin2_bin": 77,
|
||||
"latin5_bin": 78,
|
||||
"latin7_bin": 79,
|
||||
"cp850_bin": 80,
|
||||
"cp852_bin": 81,
|
||||
"swe7_bin": 82,
|
||||
"utf8_bin": 83,
|
||||
"big5_bin": 84,
|
||||
"euckr_bin": 85,
|
||||
"gb2312_bin": 86,
|
||||
"gbk_bin": 87,
|
||||
"sjis_bin": 88,
|
||||
"tis620_bin": 89,
|
||||
"ucs2_bin": 90,
|
||||
"ujis_bin": 91,
|
||||
"geostd8_general_ci": 92,
|
||||
"geostd8_bin": 93,
|
||||
"latin1_spanish_ci": 94,
|
||||
"cp932_japanese_ci": 95,
|
||||
"cp932_bin": 96,
|
||||
"eucjpms_japanese_ci": 97,
|
||||
"eucjpms_bin": 98,
|
||||
"cp1250_polish_ci": 99,
|
||||
"utf16_unicode_ci": 101,
|
||||
"utf16_icelandic_ci": 102,
|
||||
"utf16_latvian_ci": 103,
|
||||
"utf16_romanian_ci": 104,
|
||||
"utf16_slovenian_ci": 105,
|
||||
"utf16_polish_ci": 106,
|
||||
"utf16_estonian_ci": 107,
|
||||
"utf16_spanish_ci": 108,
|
||||
"utf16_swedish_ci": 109,
|
||||
"utf16_turkish_ci": 110,
|
||||
"utf16_czech_ci": 111,
|
||||
"utf16_danish_ci": 112,
|
||||
"utf16_lithuanian_ci": 113,
|
||||
"utf16_slovak_ci": 114,
|
||||
"utf16_spanish2_ci": 115,
|
||||
"utf16_roman_ci": 116,
|
||||
"utf16_persian_ci": 117,
|
||||
"utf16_esperanto_ci": 118,
|
||||
"utf16_hungarian_ci": 119,
|
||||
"utf16_sinhala_ci": 120,
|
||||
"utf16_german2_ci": 121,
|
||||
"utf16_croatian_ci": 122,
|
||||
"utf16_unicode_520_ci": 123,
|
||||
"utf16_vietnamese_ci": 124,
|
||||
"ucs2_unicode_ci": 128,
|
||||
"ucs2_icelandic_ci": 129,
|
||||
"ucs2_latvian_ci": 130,
|
||||
"ucs2_romanian_ci": 131,
|
||||
"ucs2_slovenian_ci": 132,
|
||||
"ucs2_polish_ci": 133,
|
||||
"ucs2_estonian_ci": 134,
|
||||
"ucs2_spanish_ci": 135,
|
||||
"ucs2_swedish_ci": 136,
|
||||
"ucs2_turkish_ci": 137,
|
||||
"ucs2_czech_ci": 138,
|
||||
"ucs2_danish_ci": 139,
|
||||
"ucs2_lithuanian_ci": 140,
|
||||
"ucs2_slovak_ci": 141,
|
||||
"ucs2_spanish2_ci": 142,
|
||||
"ucs2_roman_ci": 143,
|
||||
"ucs2_persian_ci": 144,
|
||||
"ucs2_esperanto_ci": 145,
|
||||
"ucs2_hungarian_ci": 146,
|
||||
"ucs2_sinhala_ci": 147,
|
||||
"ucs2_german2_ci": 148,
|
||||
"ucs2_croatian_ci": 149,
|
||||
"ucs2_unicode_520_ci": 150,
|
||||
"ucs2_vietnamese_ci": 151,
|
||||
"ucs2_general_mysql500_ci": 159,
|
||||
"utf32_unicode_ci": 160,
|
||||
"utf32_icelandic_ci": 161,
|
||||
"utf32_latvian_ci": 162,
|
||||
"utf32_romanian_ci": 163,
|
||||
"utf32_slovenian_ci": 164,
|
||||
"utf32_polish_ci": 165,
|
||||
"utf32_estonian_ci": 166,
|
||||
"utf32_spanish_ci": 167,
|
||||
"utf32_swedish_ci": 168,
|
||||
"utf32_turkish_ci": 169,
|
||||
"utf32_czech_ci": 170,
|
||||
"utf32_danish_ci": 171,
|
||||
"utf32_lithuanian_ci": 172,
|
||||
"utf32_slovak_ci": 173,
|
||||
"utf32_spanish2_ci": 174,
|
||||
"utf32_roman_ci": 175,
|
||||
"utf32_persian_ci": 176,
|
||||
"utf32_esperanto_ci": 177,
|
||||
"utf32_hungarian_ci": 178,
|
||||
"utf32_sinhala_ci": 179,
|
||||
"utf32_german2_ci": 180,
|
||||
"utf32_croatian_ci": 181,
|
||||
"utf32_unicode_520_ci": 182,
|
||||
"utf32_vietnamese_ci": 183,
|
||||
"utf8_unicode_ci": 192,
|
||||
"utf8_icelandic_ci": 193,
|
||||
"utf8_latvian_ci": 194,
|
||||
"utf8_romanian_ci": 195,
|
||||
"utf8_slovenian_ci": 196,
|
||||
"utf8_polish_ci": 197,
|
||||
"utf8_estonian_ci": 198,
|
||||
"utf8_spanish_ci": 199,
|
||||
"utf8_swedish_ci": 200,
|
||||
"utf8_turkish_ci": 201,
|
||||
"utf8_czech_ci": 202,
|
||||
"utf8_danish_ci": 203,
|
||||
"utf8_lithuanian_ci": 204,
|
||||
"utf8_slovak_ci": 205,
|
||||
"utf8_spanish2_ci": 206,
|
||||
"utf8_roman_ci": 207,
|
||||
"utf8_persian_ci": 208,
|
||||
"utf8_esperanto_ci": 209,
|
||||
"utf8_hungarian_ci": 210,
|
||||
"utf8_sinhala_ci": 211,
|
||||
"utf8_german2_ci": 212,
|
||||
"utf8_croatian_ci": 213,
|
||||
"utf8_unicode_520_ci": 214,
|
||||
"utf8_vietnamese_ci": 215,
|
||||
"utf8_general_mysql500_ci": 223,
|
||||
"utf8mb4_unicode_ci": 224,
|
||||
"utf8mb4_icelandic_ci": 225,
|
||||
"utf8mb4_latvian_ci": 226,
|
||||
"utf8mb4_romanian_ci": 227,
|
||||
"utf8mb4_slovenian_ci": 228,
|
||||
"utf8mb4_polish_ci": 229,
|
||||
"utf8mb4_estonian_ci": 230,
|
||||
"utf8mb4_spanish_ci": 231,
|
||||
"utf8mb4_swedish_ci": 232,
|
||||
"utf8mb4_turkish_ci": 233,
|
||||
"utf8mb4_czech_ci": 234,
|
||||
"utf8mb4_danish_ci": 235,
|
||||
"utf8mb4_lithuanian_ci": 236,
|
||||
"utf8mb4_slovak_ci": 237,
|
||||
"utf8mb4_spanish2_ci": 238,
|
||||
"utf8mb4_roman_ci": 239,
|
||||
"utf8mb4_persian_ci": 240,
|
||||
"utf8mb4_esperanto_ci": 241,
|
||||
"utf8mb4_hungarian_ci": 242,
|
||||
"utf8mb4_sinhala_ci": 243,
|
||||
"utf8mb4_german2_ci": 244,
|
||||
"utf8mb4_croatian_ci": 245,
|
||||
"utf8mb4_unicode_520_ci": 246,
|
||||
"utf8mb4_vietnamese_ci": 247,
|
||||
}
|
||||
|
||||
// A blacklist of collations which is unsafe to interpolate parameters.
|
||||
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
|
||||
var unsafeCollations = map[string]bool{
|
||||
"big5_chinese_ci": true,
|
||||
"sjis_japanese_ci": true,
|
||||
"gbk_chinese_ci": true,
|
||||
"big5_bin": true,
|
||||
"gb2312_bin": true,
|
||||
"gbk_bin": true,
|
||||
"sjis_bin": true,
|
||||
"cp932_japanese_ci": true,
|
||||
"cp932_bin": true,
|
||||
}
|
377
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
Normal file
377
vendor/github.com/go-sql-driver/mysql/connection.go
generated
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mysqlConn struct {
|
||||
buf buffer
|
||||
netConn net.Conn
|
||||
affectedRows uint64
|
||||
insertId uint64
|
||||
cfg *Config
|
||||
maxAllowedPacket int
|
||||
maxWriteSize int
|
||||
writeTimeout time.Duration
|
||||
flags clientFlag
|
||||
status statusFlag
|
||||
sequence uint8
|
||||
parseTime bool
|
||||
strict bool
|
||||
}
|
||||
|
||||
// Handles parameters set in DSN after the connection is established
|
||||
func (mc *mysqlConn) handleParams() (err error) {
|
||||
for param, val := range mc.cfg.Params {
|
||||
switch param {
|
||||
// Charset
|
||||
case "charset":
|
||||
charsets := strings.Split(val, ",")
|
||||
for i := range charsets {
|
||||
// ignore errors here - a charset may not exist
|
||||
err = mc.exec("SET NAMES " + charsets[i])
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// System Vars
|
||||
default:
|
||||
err = mc.exec("SET " + param + "=" + val + "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Begin() (driver.Tx, error) {
|
||||
if mc.netConn == nil {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
err := mc.exec("START TRANSACTION")
|
||||
if err == nil {
|
||||
return &mysqlTx{mc}, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Close() (err error) {
|
||||
// Makes Close idempotent
|
||||
if mc.netConn != nil {
|
||||
err = mc.writeCommandPacket(comQuit)
|
||||
}
|
||||
|
||||
mc.cleanup()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Closes the network connection and unsets internal variables. Do not call this
|
||||
// function after successfully authentication, call Close instead. This function
|
||||
// is called before auth or on auth failure because MySQL will have already
|
||||
// closed the network connection.
|
||||
func (mc *mysqlConn) cleanup() {
|
||||
// Makes cleanup idempotent
|
||||
if mc.netConn != nil {
|
||||
if err := mc.netConn.Close(); err != nil {
|
||||
errLog.Print(err)
|
||||
}
|
||||
mc.netConn = nil
|
||||
}
|
||||
mc.cfg = nil
|
||||
mc.buf.nc = nil
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Prepare(query string) (driver.Stmt, error) {
|
||||
if mc.netConn == nil {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
// Send command
|
||||
err := mc.writeCommandPacketStr(comStmtPrepare, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stmt := &mysqlStmt{
|
||||
mc: mc,
|
||||
}
|
||||
|
||||
// Read Result
|
||||
columnCount, err := stmt.readPrepareResultPacket()
|
||||
if err == nil {
|
||||
if stmt.paramCount > 0 {
|
||||
if err = mc.readUntilEOF(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if columnCount > 0 {
|
||||
err = mc.readUntilEOF()
|
||||
}
|
||||
}
|
||||
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
|
||||
// Number of ? should be same to len(args)
|
||||
if strings.Count(query, "?") != len(args) {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
|
||||
buf := mc.buf.takeCompleteBuffer()
|
||||
if buf == nil {
|
||||
// can not take the buffer. Something must be wrong with the connection
|
||||
errLog.Print(ErrBusyBuffer)
|
||||
return "", driver.ErrBadConn
|
||||
}
|
||||
buf = buf[:0]
|
||||
argPos := 0
|
||||
|
||||
for i := 0; i < len(query); i++ {
|
||||
q := strings.IndexByte(query[i:], '?')
|
||||
if q == -1 {
|
||||
buf = append(buf, query[i:]...)
|
||||
break
|
||||
}
|
||||
buf = append(buf, query[i:i+q]...)
|
||||
i += q
|
||||
|
||||
arg := args[argPos]
|
||||
argPos++
|
||||
|
||||
if arg == nil {
|
||||
buf = append(buf, "NULL"...)
|
||||
continue
|
||||
}
|
||||
|
||||
switch v := arg.(type) {
|
||||
case int64:
|
||||
buf = strconv.AppendInt(buf, v, 10)
|
||||
case float64:
|
||||
buf = strconv.AppendFloat(buf, v, 'g', -1, 64)
|
||||
case bool:
|
||||
if v {
|
||||
buf = append(buf, '1')
|
||||
} else {
|
||||
buf = append(buf, '0')
|
||||
}
|
||||
case time.Time:
|
||||
if v.IsZero() {
|
||||
buf = append(buf, "'0000-00-00'"...)
|
||||
} else {
|
||||
v := v.In(mc.cfg.Loc)
|
||||
v = v.Add(time.Nanosecond * 500) // To round under microsecond
|
||||
year := v.Year()
|
||||
year100 := year / 100
|
||||
year1 := year % 100
|
||||
month := v.Month()
|
||||
day := v.Day()
|
||||
hour := v.Hour()
|
||||
minute := v.Minute()
|
||||
second := v.Second()
|
||||
micro := v.Nanosecond() / 1000
|
||||
|
||||
buf = append(buf, []byte{
|
||||
'\'',
|
||||
digits10[year100], digits01[year100],
|
||||
digits10[year1], digits01[year1],
|
||||
'-',
|
||||
digits10[month], digits01[month],
|
||||
'-',
|
||||
digits10[day], digits01[day],
|
||||
' ',
|
||||
digits10[hour], digits01[hour],
|
||||
':',
|
||||
digits10[minute], digits01[minute],
|
||||
':',
|
||||
digits10[second], digits01[second],
|
||||
}...)
|
||||
|
||||
if micro != 0 {
|
||||
micro10000 := micro / 10000
|
||||
micro100 := micro / 100 % 100
|
||||
micro1 := micro % 100
|
||||
buf = append(buf, []byte{
|
||||
'.',
|
||||
digits10[micro10000], digits01[micro10000],
|
||||
digits10[micro100], digits01[micro100],
|
||||
digits10[micro1], digits01[micro1],
|
||||
}...)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
}
|
||||
case []byte:
|
||||
if v == nil {
|
||||
buf = append(buf, "NULL"...)
|
||||
} else {
|
||||
buf = append(buf, "_binary'"...)
|
||||
if mc.status&statusNoBackslashEscapes == 0 {
|
||||
buf = escapeBytesBackslash(buf, v)
|
||||
} else {
|
||||
buf = escapeBytesQuotes(buf, v)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
}
|
||||
case string:
|
||||
buf = append(buf, '\'')
|
||||
if mc.status&statusNoBackslashEscapes == 0 {
|
||||
buf = escapeStringBackslash(buf, v)
|
||||
} else {
|
||||
buf = escapeStringQuotes(buf, v)
|
||||
}
|
||||
buf = append(buf, '\'')
|
||||
default:
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
|
||||
if len(buf)+4 > mc.maxAllowedPacket {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
}
|
||||
if argPos != len(args) {
|
||||
return "", driver.ErrSkip
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||
if mc.netConn == nil {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
if len(args) != 0 {
|
||||
if !mc.cfg.InterpolateParams {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
// try to interpolate the parameters to save extra roundtrips for preparing and closing a statement
|
||||
prepared, err := mc.interpolateParams(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = prepared
|
||||
args = nil
|
||||
}
|
||||
mc.affectedRows = 0
|
||||
mc.insertId = 0
|
||||
|
||||
err := mc.exec(query)
|
||||
if err == nil {
|
||||
return &mysqlResult{
|
||||
affectedRows: int64(mc.affectedRows),
|
||||
insertId: int64(mc.insertId),
|
||||
}, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Internal function to execute commands
|
||||
func (mc *mysqlConn) exec(query string) error {
|
||||
// Send command
|
||||
err := mc.writeCommandPacketStr(comQuery, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read Result
|
||||
resLen, err := mc.readResultSetHeaderPacket()
|
||||
if err == nil && resLen > 0 {
|
||||
if err = mc.readUntilEOF(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = mc.readUntilEOF()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mc *mysqlConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||
if mc.netConn == nil {
|
||||
errLog.Print(ErrInvalidConn)
|
||||
return nil, driver.ErrBadConn
|
||||
}
|
||||
if len(args) != 0 {
|
||||
if !mc.cfg.InterpolateParams {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
// try client-side prepare to reduce roundtrip
|
||||
prepared, err := mc.interpolateParams(query, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = prepared
|
||||
args = nil
|
||||
}
|
||||
// Send command
|
||||
err := mc.writeCommandPacketStr(comQuery, query)
|
||||
if err == nil {
|
||||
// Read Result
|
||||
var resLen int
|
||||
resLen, err = mc.readResultSetHeaderPacket()
|
||||
if err == nil {
|
||||
rows := new(textRows)
|
||||
rows.mc = mc
|
||||
|
||||
if resLen == 0 {
|
||||
// no columns, no more data
|
||||
return emptyRows{}, nil
|
||||
}
|
||||
// Columns
|
||||
rows.columns, err = mc.readColumns(resLen)
|
||||
return rows, err
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Gets the value of the given MySQL System Variable
|
||||
// The returned byte slice is only valid until the next read
|
||||
func (mc *mysqlConn) getSystemVar(name string) ([]byte, error) {
|
||||
// Send command
|
||||
if err := mc.writeCommandPacketStr(comQuery, "SELECT @@"+name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read Result
|
||||
resLen, err := mc.readResultSetHeaderPacket()
|
||||
if err == nil {
|
||||
rows := new(textRows)
|
||||
rows.mc = mc
|
||||
rows.columns = []mysqlField{{fieldType: fieldTypeVarChar}}
|
||||
|
||||
if resLen > 0 {
|
||||
// Columns
|
||||
if err := mc.readUntilEOF(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dest := make([]driver.Value, resLen)
|
||||
if err = rows.readRow(dest); err == nil {
|
||||
return dest[0].([]byte), mc.readUntilEOF()
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
163
vendor/github.com/go-sql-driver/mysql/const.go
generated
vendored
Normal file
163
vendor/github.com/go-sql-driver/mysql/const.go
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
|
||||
//
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package mysql
|
||||
|
||||
const (
|
||||
minProtocolVersion byte = 10
|
||||
maxPacketSize = 1<<24 - 1
|
||||
timeFormat = "2006-01-02 15:04:05.999999"
|
||||
)
|
||||
|
||||
// MySQL constants documentation:
|
||||
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
|
||||
|
||||
const (
|
||||
iOK byte = 0x00
|
||||
iLocalInFile byte = 0xfb
|
||||
iEOF byte = 0xfe
|
||||
iERR byte = 0xff
|
||||
)
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
|
||||
type clientFlag uint32
|
||||
|
||||
const (
|
||||
clientLongPassword clientFlag = 1 << iota
|
||||
clientFoundRows
|
||||
clientLongFlag
|
||||
clientConnectWithDB
|
||||
clientNoSchema
|
||||
clientCompress
|
||||
clientODBC
|
||||
clientLocalFiles
|
||||
clientIgnoreSpace
|
||||
clientProtocol41
|
||||
clientInteractive
|
||||
clientSSL
|
||||
clientIgnoreSIGPIPE
|
||||
clientTransactions
|
||||
clientReserved
|
||||
clientSecureConn
|
||||
clientMultiStatements
|
||||
clientMultiResults
|
||||
clientPSMultiResults
|
||||
clientPluginAuth
|
||||
clientConnectAttrs
|
||||
clientPluginAuthLenEncClientData
|
||||
clientCanHandleExpiredPasswords
|
||||
clientSessionTrack
|
||||
clientDeprecateEOF
|
||||
)
|
||||
|
||||
const (
|
||||
comQuit byte = iota + 1
|
||||
comInitDB
|
||||
comQuery
|
||||
comFieldList
|
||||
comCreateDB
|
||||
comDropDB
|
||||
comRefresh
|
||||
comShutdown
|
||||
comStatistics
|
||||
comProcessInfo
|
||||
comConnect
|
||||
comProcessKill
|
||||
comDebug
|
||||
comPing
|
||||
comTime
|
||||
comDelayedInsert
|
||||
comChangeUser
|
||||
comBinlogDump
|
||||
comTableDump
|
||||
comConnectOut
|
||||
comRegisterSlave
|
||||
comStmtPrepare
|
||||
comStmtExecute
|
||||
comStmtSendLongData
|
||||
comStmtClose
|
||||
comStmtReset
|
||||
comSetOption
|
||||
comStmtFetch
|
||||
)
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
|
||||
const (
|
||||
fieldTypeDecimal byte = iota
|
||||
fieldTypeTiny
|
||||
fieldTypeShort
|
||||
fieldTypeLong
|
||||
fieldTypeFloat
|
||||
fieldTypeDouble
|
||||
fieldTypeNULL
|
||||
fieldTypeTimestamp
|
||||
fieldTypeLongLong
|
||||
fieldTypeInt24
|
||||
fieldTypeDate
|
||||
fieldTypeTime
|
||||
fieldTypeDateTime
|
||||
fieldTypeYear
|
||||
fieldTypeNewDate
|
||||
fieldTypeVarChar
|
||||
fieldTypeBit
|
||||
)
|
||||
const (
|
||||
fieldTypeJSON byte = iota + 0xf5
|
||||
fieldTypeNewDecimal
|
||||
fieldTypeEnum
|
||||
fieldTypeSet
|
||||
fieldTypeTinyBLOB
|
||||
fieldTypeMediumBLOB
|
||||
fieldTypeLongBLOB
|
||||
fieldTypeBLOB
|
||||
fieldTypeVarString
|
||||
fieldTypeString
|
||||
fieldTypeGeometry
|
||||
)
|
||||
|
||||
type fieldFlag uint16
|
||||
|
||||
const (
|
||||
flagNotNULL fieldFlag = 1 << iota
|
||||
flagPriKey
|
||||
flagUniqueKey
|
||||
flagMultipleKey
|
||||
flagBLOB
|
||||
flagUnsigned
|
||||
flagZeroFill
|
||||
flagBinary
|
||||
flagEnum
|
||||
flagAutoIncrement
|
||||
flagTimestamp
|
||||
flagSet
|
||||
flagUnknown1
|
||||
flagUnknown2
|
||||
flagUnknown3
|
||||
flagUnknown4
|
||||
)
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/status-flags.html
|
||||
type statusFlag uint16
|
||||
|
||||
const (
|
||||
statusInTrans statusFlag = 1 << iota
|
||||
statusInAutocommit
|
||||
statusReserved // Not in documentation
|
||||
statusMoreResultsExists
|
||||
statusNoGoodIndexUsed
|
||||
statusNoIndexUsed
|
||||
statusCursorExists
|
||||
statusLastRowSent
|
||||
statusDbDropped
|
||||
statusNoBackslashEscapes
|
||||
statusMetadataChanged
|
||||
statusQueryWasSlow
|
||||
statusPsOutParams
|
||||
statusInTransReadonly
|
||||
statusSessionStateChanged
|
||||
)
|
183
vendor/github.com/go-sql-driver/mysql/driver.go
generated
vendored
Normal file
183
vendor/github.com/go-sql-driver/mysql/driver.go
generated
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// Package mysql provides a MySQL driver for Go's database/sql package
|
||||
//
|
||||
// The driver should be used via the database/sql package:
|
||||
//
|
||||
// import "database/sql"
|
||||
// import _ "github.com/go-sql-driver/mysql"
|
||||
//
|
||||
// db, err := sql.Open("mysql", "user:password@/dbname")
|
||||
//
|
||||
// See https://github.com/go-sql-driver/mysql#usage for details
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"net"
|
||||
)
|
||||
|
||||
// MySQLDriver is exported to make the driver directly accessible.
|
||||
// In general the driver is used via the database/sql package.
|
||||
type MySQLDriver struct{}
|
||||
|
||||
// DialFunc is a function which can be used to establish the network connection.
|
||||
// Custom dial functions must be registered with RegisterDial
|
||||
type DialFunc func(addr string) (net.Conn, error)
|
||||
|
||||
var dials map[string]DialFunc
|
||||
|
||||
// RegisterDial registers a custom dial function. It can then be used by the
|
||||
// network address mynet(addr), where mynet is the registered new network.
|
||||
// addr is passed as a parameter to the dial function.
|
||||
func RegisterDial(net string, dial DialFunc) {
|
||||
if dials == nil {
|
||||
dials = make(map[string]DialFunc)
|
||||
}
|
||||
dials[net] = dial
|
||||
}
|
||||
|
||||
// Open new Connection.
|
||||
// See https://github.com/go-sql-driver/mysql#dsn-data-source-name for how
|
||||
// the DSN string is formated
|
||||
func (d MySQLDriver) Open(dsn string) (driver.Conn, error) {
|
||||
var err error
|
||||
|
||||
// New mysqlConn
|
||||
mc := &mysqlConn{
|
||||
maxAllowedPacket: maxPacketSize,
|
||||
maxWriteSize: maxPacketSize - 1,
|
||||
}
|
||||
mc.cfg, err = ParseDSN(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mc.parseTime = mc.cfg.ParseTime
|
||||
mc.strict = mc.cfg.Strict
|
||||
|
||||
// Connect to Server
|
||||
if dial, ok := dials[mc.cfg.Net]; ok {
|
||||
mc.netConn, err = dial(mc.cfg.Addr)
|
||||
} else {
|
||||
nd := net.Dialer{Timeout: mc.cfg.Timeout}
|
||||
mc.netConn, err = nd.Dial(mc.cfg.Net, mc.cfg.Addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enable TCP Keepalives on TCP connections
|
||||
if tc, ok := mc.netConn.(*net.TCPConn); ok {
|
||||
if err := tc.SetKeepAlive(true); err != nil {
|
||||
// Don't send COM_QUIT before handshake.
|
||||
mc.netConn.Close()
|
||||
mc.netConn = nil
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
mc.buf = newBuffer(mc.netConn)
|
||||
|
||||
// Set I/O timeouts
|
||||
mc.buf.timeout = mc.cfg.ReadTimeout
|
||||
mc.writeTimeout = mc.cfg.WriteTimeout
|
||||
|
||||
// Reading Handshake Initialization Packet
|
||||
cipher, err := mc.readInitPacket()
|
||||
if err != nil {
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send Client Authentication Packet
|
||||
if err = mc.writeAuthPacket(cipher); err != nil {
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle response to auth packet, switch methods if possible
|
||||
if err = handleAuthResult(mc, cipher); err != nil {
|
||||
// Authentication failed and MySQL has already closed the connection
|
||||
// (https://dev.mysql.com/doc/internals/en/authentication-fails.html).
|
||||
// Do not send COM_QUIT, just cleanup and return the error.
|
||||
mc.cleanup()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mc.cfg.MaxAllowedPacket > 0 {
|
||||
mc.maxAllowedPacket = mc.cfg.MaxAllowedPacket
|
||||
} else {
|
||||
// Get max allowed packet size
|
||||
maxap, err := mc.getSystemVar("max_allowed_packet")
|
||||
if err != nil {
|
||||
mc.Close()
|
||||
return nil, err
|
||||
}
|
||||
mc.maxAllowedPacket = stringToInt(maxap) - 1
|
||||
}
|
||||
if mc.maxAllowedPacket < maxPacketSize {
|
||||
mc.maxWriteSize = mc.maxAllowedPacket
|
||||
}
|
||||
|
||||
// Handle DSN Params
|
||||
err = mc.handleParams()
|
||||
if err != nil {
|
||||
mc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
func handleAuthResult(mc *mysqlConn, oldCipher []byte) error {
|
||||
// Read Result Packet
|
||||
cipher, err := mc.readResultOK()
|
||||
if err == nil {
|
||||
return nil // auth successful
|
||||
}
|
||||
|
||||
if mc.cfg == nil {
|
||||
return err // auth failed and retry not possible
|
||||
}
|
||||
|
||||
// Retry auth if configured to do so.
|
||||
if mc.cfg.AllowOldPasswords && err == ErrOldPassword {
|
||||
// Retry with old authentication method. Note: there are edge cases
|
||||
// where this should work but doesn't; this is currently "wontfix":
|
||||
// https://github.com/go-sql-driver/mysql/issues/184
|
||||
|
||||
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is
|
||||
// sent and we have to keep using the cipher sent in the init packet.
|
||||
if cipher == nil {
|
||||
cipher = oldCipher
|
||||
}
|
||||
|
||||
if err = mc.writeOldAuthPacket(cipher); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
} else if mc.cfg.AllowCleartextPasswords && err == ErrCleartextPassword {
|
||||
// Retry with clear text password for
|
||||
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html
|
||||
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html
|
||||
if err = mc.writeClearAuthPacket(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
} else if mc.cfg.AllowNativePasswords && err == ErrNativePassword {
|
||||
if err = mc.writeNativeAuthPacket(cipher); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mc.readResultOK()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func init() {
|
||||
sql.Register("mysql", &MySQLDriver{})
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user