initial commit
This commit is contained in:
commit
18995db757
871 changed files with 492725 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
gowiki
|
||||||
|
tmp
|
||||||
|
run.sh
|
||||||
|
TODO.txt
|
||||||
14
COPYRIGHT
Normal file
14
COPYRIGHT
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
|
|
@ -0,0 +1,674 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 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 General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is 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. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
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 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. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
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 Affero 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 special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU 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 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 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 General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||||
49
Makefile
Normal file
49
Makefile
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
CC=go
|
||||||
|
BUILD=build -v
|
||||||
|
VERSION=$(shell cat VERSION)
|
||||||
|
GCFLAGS=-gcflags '-e'
|
||||||
|
LDFLAGS=-ldflags '-X main.version=$(VERSION) -s -w'
|
||||||
|
PROGRAM=gowiki
|
||||||
|
ENV=CGO_ENABLED=0
|
||||||
|
|
||||||
|
all: $(PROGRAM)
|
||||||
|
|
||||||
|
setup:
|
||||||
|
$(CC) get github.com/client9/misspell
|
||||||
|
|
||||||
|
vendor: clean codeqa
|
||||||
|
$(ENV) $(CC) $(BUILD) -mod=vendor $(LDFLAGS)
|
||||||
|
|
||||||
|
$(PROGRAM): clean codeqa
|
||||||
|
$(ENV) $(CC) $(BUILD) $(LDFLAGS)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(CC) clean -x
|
||||||
|
|
||||||
|
codeqa: generate gofmt misspell golint test
|
||||||
|
|
||||||
|
generate:
|
||||||
|
$(CC) generate
|
||||||
|
|
||||||
|
gofmt:
|
||||||
|
gofmt -w pkg/ main.go
|
||||||
|
|
||||||
|
golint:
|
||||||
|
$(GOPATH)/bin/golint .
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
# ignore misspellings from vendor/
|
||||||
|
$(GOPATH)/bin/misspell -i functionallity,avaliable,HALP,halp,agains,becuase,Christiaan,compatiblity,conjuction,frequence,inital,inot,occurance,overriden,pervious,preceeded,preceeds,precendence,PROPOGATE,reamining,represenation,soley,substract,supression,whcih,preceed *
|
||||||
|
|
||||||
|
test:
|
||||||
|
ifeq ($(shell go env GOARCH), $(shell go env GOHOSTARCH))
|
||||||
|
ifeq ($(shell go env GOOS), $(shell go env GOHOSTOS))
|
||||||
|
$(CC) test ./...
|
||||||
|
else
|
||||||
|
$(info skipping tests of other platforms)
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
$(info skipping tests of other platforms)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: setup vendor build clean codeqa generate gofmt golint misspell test
|
||||||
27
README.md
Normal file
27
README.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# GoWiki - a simple wiki engine written in Go
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
make
|
||||||
|
|
||||||
|
or offline
|
||||||
|
|
||||||
|
make vendor
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenBSD:
|
||||||
|
|
||||||
|
```
|
||||||
|
GOOS=openbsd make
|
||||||
|
|
||||||
|
or offline
|
||||||
|
|
||||||
|
GOOS=openbsd make vendor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
See the `scripts/` directory on how to install gowiki on Linux or OpenBSD.
|
||||||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
v2.0
|
||||||
24
go.mod
Normal file
24
go.mod
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
module git.giftfish.de/ston1th/docstore
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.giftfish.de/ston1th/godrop/v2 v2.1.0
|
||||||
|
git.giftfish.de/ston1th/jwt/v3 v3.2.0
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.17 // indirect
|
||||||
|
github.com/Smerity/govarint v0.0.0-20150407073650-7265e41f48f1 // indirect
|
||||||
|
github.com/blevesearch/bleve v0.7.0
|
||||||
|
github.com/blevesearch/go-porterstemmer v1.0.2 // indirect
|
||||||
|
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f // indirect
|
||||||
|
github.com/boltdb/bolt v1.3.1
|
||||||
|
github.com/boombuler/barcode v1.0.0 // indirect
|
||||||
|
github.com/couchbase/vellum v0.0.0-20190328134517-462e86d8716b // indirect
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0 // indirect
|
||||||
|
github.com/golang/protobuf v1.3.1 // indirect
|
||||||
|
github.com/gorilla/mux v1.7.1
|
||||||
|
github.com/pquerna/otp v1.1.0
|
||||||
|
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect
|
||||||
|
github.com/urfave/cli v1.20.0
|
||||||
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480
|
||||||
|
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba // indirect
|
||||||
|
)
|
||||||
63
go.sum
Normal file
63
go.sum
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
git.giftfish.de/ston1th/godrop/v2 v2.1.0 h1:vR4aWVeMGJ+tHQH0p44esZrSl5dYJzuCmZqhKWP/vwI=
|
||||||
|
git.giftfish.de/ston1th/godrop/v2 v2.1.0/go.mod h1:z8KA3HP/vB04wddLXZfYVKURYDxBGau9hlHtV61x9mM=
|
||||||
|
git.giftfish.de/ston1th/gowiki v0.0.0-20170406191448-a3270f399d54 h1:5Xo8nWnlHnZPM5plIfu8WEgTPwtg0nyimQ8uk+RUypM=
|
||||||
|
git.giftfish.de/ston1th/jwt/v3 v3.2.0 h1:V6Lh7OjXn4D3HPd/LoDf7w5N7cyiSXEPAQCIfSVZ05c=
|
||||||
|
git.giftfish.de/ston1th/jwt/v3 v3.2.0/go.mod h1:jDALrW7OKeIaqYuMxs3GWyPRnQZmk/R+Lu9p/Em3Ziw=
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.17 h1:oCYFIFEMSQZrLHpywH7919esI1VSrQZ0pJXkZPGIJ78=
|
||||||
|
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
|
||||||
|
github.com/Smerity/govarint v0.0.0-20150407073650-7265e41f48f1 h1:G/NOANWMQev0CftoyxQwtRakdyNNNMB3qxkt/tj1HGs=
|
||||||
|
github.com/Smerity/govarint v0.0.0-20150407073650-7265e41f48f1/go.mod h1:o80NPAib/LOl8Eysqppjj7kkGkqz++eqzYGlvROpDcQ=
|
||||||
|
github.com/blevesearch/bleve v0.7.0 h1:znyZ3zjsh2Scr60vszs7rbF29TU6i1q9bfnZf1vh0Ac=
|
||||||
|
github.com/blevesearch/bleve v0.7.0/go.mod h1:Y2lmIkzV6mcNfAnAdOd+ZxHkHchhBfU/xroGIp61wfw=
|
||||||
|
github.com/blevesearch/go-porterstemmer v1.0.2 h1:qe7n69gBd1OLY5sHKnxQHIbzn0LNJA4hpAf+5XDxV2I=
|
||||||
|
github.com/blevesearch/go-porterstemmer v1.0.2/go.mod h1:haWQqFT3RdOGz7PJuM3or/pWNJS1pKkoZJWCkWu0DVA=
|
||||||
|
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f h1:kqbi9lqXLLs+zfWlgo1PIiRQ86n33K1JKotjj4rSYOg=
|
||||||
|
github.com/blevesearch/segment v0.0.0-20160915185041-762005e7a34f/go.mod h1:IInt5XRvpiGE09KOk9mmCMLjHhydIhNPKPPFLFBB7L8=
|
||||||
|
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||||
|
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||||
|
github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc=
|
||||||
|
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/couchbase/vellum v0.0.0-20190328134517-462e86d8716b h1:WgO2yt0pCXyPgU3K/pnY1qO/8FP8TxBAaYXqa+BQpx0=
|
||||||
|
github.com/couchbase/vellum v0.0.0-20190328134517-462e86d8716b/go.mod h1:prYTC8EgTu3gwbqJihkud9zRXISvyulAplQ6exdCo1g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
|
||||||
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2 h1:Ujru1hufTHVb++eG6OuNDKMxZnGIvF6o/u8q/8h2+I4=
|
||||||
|
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||||
|
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU=
|
||||||
|
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||||
|
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
|
||||||
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.1.0 h1:q2gMsMuMl3JzneUaAX1MRGxLvOG6bzXV51hivBaStf0=
|
||||||
|
github.com/pquerna/otp v1.1.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||||
|
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 h1:JNEGSiWg6D3lcBCMCBqN3ELniXujt+0QNHLhNnO0w3s=
|
||||||
|
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2/go.mod h1:mjqs7N0Q6m5HpR7QfXVBZXZWSqTjQLeTujjA/xUp2uw=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||||
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
|
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||||
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
|
github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
|
||||||
|
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk=
|
||||||
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
|
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba h1:h0zCzEL5UW1mERvwTN6AXcc75PpLkY6OcReia6Dq1BM=
|
||||||
|
golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20181026144532-2772b66316d2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
13
main.go
Normal file
13
main.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
//go:generate ./templates.sh
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.giftfish.de/ston1th/docstore/pkg/cmd"
|
||||||
|
|
||||||
|
var version string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Run(version)
|
||||||
|
}
|
||||||
275
pkg/cmd/cmd.go
Normal file
275
pkg/cmd/cmd.go
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/db"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/server"
|
||||||
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
stdlog "log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defDataDir = "/var/lib/docstore"
|
||||||
|
defListen = "127.0.0.1:8080"
|
||||||
|
defLogFile = "docstore.log"
|
||||||
|
|
||||||
|
defRunUser = "wiki"
|
||||||
|
defRunGroup = "wiki"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
config core.Config
|
||||||
|
dumpFile string
|
||||||
|
configFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
func initServer(cfg core.Config) (err error) {
|
||||||
|
// remove initial pledge due to violation with go1.12
|
||||||
|
// sysctl kern.somaxconn
|
||||||
|
//if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec id unveil"); err != nil {
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
dropCfg := godrop.Config{
|
||||||
|
User: cfg.User,
|
||||||
|
Group: cfg.Group,
|
||||||
|
Foreground: cfg.Foreground,
|
||||||
|
}
|
||||||
|
err = godrop.Drop(dropCfg, func() (net.Listener, error) {
|
||||||
|
return net.Listen("tcp", cfg.ListenAddr)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = os.Chdir(cfg.DataDir); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock unveil"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := godrop.GetListener()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InitLogger(cfg)
|
||||||
|
|
||||||
|
srv := server.NewHTTPServer(cfg, l)
|
||||||
|
err = srv.Start()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("docstore " + cfg.Version + " started")
|
||||||
|
sigs := make(chan os.Signal)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
sig := <-sigs
|
||||||
|
log.Println("signal: " + sig.String())
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Second * 20)
|
||||||
|
log.Fatal("stop timed out: killing")
|
||||||
|
}()
|
||||||
|
err = srv.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
log.Println("docstore stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() error {
|
||||||
|
if configFile == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
file, err := os.Open(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(file).Decode(&config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(version string) {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "docstore"
|
||||||
|
app.Usage = "a simple wiki engine"
|
||||||
|
app.Version = version
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "server",
|
||||||
|
Usage: "start docstore server",
|
||||||
|
Flags: defaultFlags(serverFlags()),
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
if err := loadConfig(); err != nil {
|
||||||
|
stdlog.Fatal("server: ", err)
|
||||||
|
}
|
||||||
|
config.Version = app.Version
|
||||||
|
if err := initServer(config); err != nil {
|
||||||
|
stdlog.Fatal("server: ", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "dump",
|
||||||
|
Usage: "create a json dump of all articles in the data directory (docstore needs to be stopped)",
|
||||||
|
Flags: defaultFlags(dumpRestoreFlags()),
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
var (
|
||||||
|
file = os.Stdout
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err = loadConfig(); err != nil {
|
||||||
|
stdlog.Fatal("dump: ", err)
|
||||||
|
}
|
||||||
|
if dumpFile != "-" {
|
||||||
|
file, err = os.OpenFile(dumpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640)
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("dump: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB, err := db.New(config)
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("dump: ", err)
|
||||||
|
}
|
||||||
|
if err = DB.Dump(file); err != nil {
|
||||||
|
stdlog.Fatal("dump: ", err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
err = DB.Close()
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("dump: ", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "restore",
|
||||||
|
Usage: "restore a json dump to the data directory (docstore needs to be stopped)",
|
||||||
|
Flags: defaultFlags(dumpRestoreFlags()),
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
var (
|
||||||
|
file = os.Stdin
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if err = loadConfig(); err != nil {
|
||||||
|
stdlog.Fatal("restore: ", err)
|
||||||
|
}
|
||||||
|
if dumpFile != "-" {
|
||||||
|
file, err = os.Open(dumpFile)
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("restore: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DB, err := db.New(config)
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("restore: ", err)
|
||||||
|
}
|
||||||
|
if err = DB.Restore(file); err != nil {
|
||||||
|
stdlog.Fatal("restore: ", err)
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
err = DB.Close()
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal("restore: ", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverFlags() []cli.Flag {
|
||||||
|
return []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "user, u",
|
||||||
|
Value: defRunUser,
|
||||||
|
Usage: "drop privileges to user",
|
||||||
|
Destination: &config.User,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "group, g",
|
||||||
|
Value: defRunGroup,
|
||||||
|
Usage: "drop privileges to group",
|
||||||
|
Destination: &config.Group,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "listen, l",
|
||||||
|
Value: defListen,
|
||||||
|
Usage: "listening <address>:<port>",
|
||||||
|
Destination: &config.ListenAddr,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "log",
|
||||||
|
Value: defLogFile,
|
||||||
|
Usage: "log file (use - for stdout, this only works in combination with -f)",
|
||||||
|
Destination: &config.LogFile,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "secret",
|
||||||
|
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
||||||
|
Destination: &config.Secret,
|
||||||
|
},
|
||||||
|
cli.BoolTFlag{
|
||||||
|
Name: "admin, a",
|
||||||
|
Usage: "use -a=false to disable the admin user",
|
||||||
|
Destination: &config.Admin,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "foreground, f",
|
||||||
|
Usage: "do not fork into the background",
|
||||||
|
Destination: &config.Foreground,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "secure, s",
|
||||||
|
Usage: "enable secure cookie",
|
||||||
|
Destination: &config.SecureCookie,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "debug",
|
||||||
|
Usage: "enable debugging",
|
||||||
|
Destination: &config.Debug,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpRestoreFlags() []cli.Flag {
|
||||||
|
return []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "file, f",
|
||||||
|
Value: "-",
|
||||||
|
Usage: "file to read/write dump (default stdin/stdout)",
|
||||||
|
Destination: &dumpFile,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFlags(f []cli.Flag) []cli.Flag {
|
||||||
|
return append([]cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "config, c",
|
||||||
|
Usage: "config file",
|
||||||
|
Destination: &configFile,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "data, d",
|
||||||
|
Value: defDataDir,
|
||||||
|
Usage: "data directory",
|
||||||
|
Destination: &config.DataDir,
|
||||||
|
},
|
||||||
|
}, f...)
|
||||||
|
}
|
||||||
14
pkg/core/const.go
Normal file
14
pkg/core/const.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
var (
|
||||||
|
IndexURI = "/"
|
||||||
|
TotpURI = "/totp"
|
||||||
|
LoginURI = "/login"
|
||||||
|
LogoutURI = "/logout"
|
||||||
|
|
||||||
|
AutoImport = "auto_import"
|
||||||
|
ManualImport = "manual_import"
|
||||||
|
Imported = "imported"
|
||||||
|
)
|
||||||
26
pkg/core/helper.go
Normal file
26
pkg/core/helper.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Contains checks if slice a contains string str
|
||||||
|
func Contains(str string, a []string) (int, bool) {
|
||||||
|
for i, s := range a {
|
||||||
|
if s == str {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeFmt = "2006-01-02 15:04:05"
|
||||||
|
fileFmt = "20060102_150405"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Now() string {
|
||||||
|
return time.Now().Format(timeFmt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileName() string {
|
||||||
|
return time.Now().Format(fileFmt)
|
||||||
|
}
|
||||||
41
pkg/core/permission.go
Normal file
41
pkg/core/permission.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
type Permission int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Invalid Permission = iota
|
||||||
|
Public
|
||||||
|
Private
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParsePermString(p string) Permission {
|
||||||
|
switch p {
|
||||||
|
case "1":
|
||||||
|
return Public
|
||||||
|
case "2":
|
||||||
|
return Private
|
||||||
|
}
|
||||||
|
return Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadPerm(username string, p Permission, s Section) bool {
|
||||||
|
switch p {
|
||||||
|
case Public:
|
||||||
|
return true
|
||||||
|
case Private:
|
||||||
|
_, c := Contains(username, s.Members)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func WritePerm(username string, p Permission, s Section) bool {
|
||||||
|
switch p {
|
||||||
|
case Public, Private:
|
||||||
|
_, c := Contains(username, s.Members)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
77
pkg/core/permission_test.go
Normal file
77
pkg/core/permission_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePermString(t *testing.T) {
|
||||||
|
data := map[string]Permission{
|
||||||
|
"": Invalid,
|
||||||
|
"1": Public,
|
||||||
|
"2": Private,
|
||||||
|
"123": Invalid,
|
||||||
|
"test": Invalid,
|
||||||
|
}
|
||||||
|
for k, v := range data {
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
if ParsePermString(k) != v {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPerm(t *testing.T) {
|
||||||
|
data := []struct {
|
||||||
|
Username string
|
||||||
|
Perm Permission
|
||||||
|
Section Section
|
||||||
|
OK bool
|
||||||
|
}{
|
||||||
|
{"", Invalid, Section{Members: []string{"admin"}}, false},
|
||||||
|
{"", Public, Section{Members: []string{"admin"}}, true},
|
||||||
|
{"user", Public, Section{Members: []string{"admin"}}, true},
|
||||||
|
{"", Private, Section{Members: []string{"admin"}}, false},
|
||||||
|
{"user", Private, Section{Members: []string{"admin"}}, false},
|
||||||
|
{"", Private, Section{Members: []string{"user"}}, false},
|
||||||
|
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||||
|
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||||
|
{"admin", Private, Section{Members: []string{"user"}}, false},
|
||||||
|
}
|
||||||
|
for i, v := range data {
|
||||||
|
t.Run(fmt.Sprintf("Read:%d", i), func(t *testing.T) {
|
||||||
|
if ReadPerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWritePerm(t *testing.T) {
|
||||||
|
data := []struct {
|
||||||
|
Username string
|
||||||
|
Perm Permission
|
||||||
|
Section Section
|
||||||
|
OK bool
|
||||||
|
}{
|
||||||
|
{"", Invalid, Section{Members: []string{"user"}}, false},
|
||||||
|
{"", Public, Section{Members: []string{"user"}}, false},
|
||||||
|
{"user", Public, Section{Members: []string{"user"}}, true},
|
||||||
|
{"user", Public, Section{Members: []string{"admin"}}, false},
|
||||||
|
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||||
|
{"", Private, Section{Members: []string{"user"}}, false},
|
||||||
|
{"", Private, Section{Members: []string{"user", "admin"}}, false},
|
||||||
|
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||||
|
{"admin", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||||
|
}
|
||||||
|
for i, v := range data {
|
||||||
|
t.Run(fmt.Sprintf("Write:%d", i), func(t *testing.T) {
|
||||||
|
if WritePerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
pkg/core/timeout.go
Normal file
31
pkg/core/timeout.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) {
|
||||||
|
done := make(chan error)
|
||||||
|
out := make(chan []byte)
|
||||||
|
go func() {
|
||||||
|
o, e := cmd.Output()
|
||||||
|
done <- e
|
||||||
|
out <- o
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-time.After(t):
|
||||||
|
e := cmd.Process.Kill()
|
||||||
|
<-out
|
||||||
|
if e != nil {
|
||||||
|
<-done
|
||||||
|
err = e
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = <-done
|
||||||
|
case e := <-done:
|
||||||
|
err = e
|
||||||
|
output = <-out
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
76
pkg/core/types.go
Normal file
76
pkg/core/types.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import "html/template"
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoreTitle string `json:"store_title"`
|
||||||
|
Markdown string `json:"markdown"`
|
||||||
|
TOC template.HTML `json:"toc"`
|
||||||
|
HTML template.HTML `json:"html"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Updated string `json:"updated"`
|
||||||
|
Perm Permission `json:"perm"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pages []Page
|
||||||
|
|
||||||
|
func (p Pages) Len() int { return len(p) }
|
||||||
|
func (p Pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
func (p Pages) Less(i, j int) bool { return p[i].StoreTitle < p[j].StoreTitle }
|
||||||
|
|
||||||
|
type UpdatedPages []Page
|
||||||
|
|
||||||
|
func (p UpdatedPages) Len() int { return len(p) }
|
||||||
|
func (p UpdatedPages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
func (p UpdatedPages) Less(i, j int) bool { return p[i].Updated > p[j].Updated }
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
Name string
|
||||||
|
User bool
|
||||||
|
Members []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sections []Section
|
||||||
|
|
||||||
|
func (s Sections) Len() int { return len(s) }
|
||||||
|
func (s Sections) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s Sections) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||||
|
|
||||||
|
type SectionPage struct {
|
||||||
|
Section string
|
||||||
|
Pages UpdatedPages
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionPages []SectionPage
|
||||||
|
|
||||||
|
func (s SectionPages) Len() int { return len(s) }
|
||||||
|
func (s SectionPages) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s SectionPages) Less(i, j int) bool { return s[i].Section < s[j].Section }
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Secret string
|
||||||
|
Locked int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
ID string
|
||||||
|
HTML template.HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DataDir string `json:"data_dir,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
Group string `json:"group,omitempty"`
|
||||||
|
ListenAddr string `json:"listen_addr,omitempty"`
|
||||||
|
LogFile string `json:"log_file,omitempty"`
|
||||||
|
Version string `json:"-"`
|
||||||
|
Secret string `json:"secret,omitempty"`
|
||||||
|
Foreground bool `json:"foreground,omitempty"`
|
||||||
|
SecureCookie bool `json:"secure_cookie,omitempty"`
|
||||||
|
Debug bool `json:"debug,omitempty"`
|
||||||
|
}
|
||||||
36
pkg/db/blacklist.go
Normal file
36
pkg/db/blacklist.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "git.giftfish.de/ston1th/jwt/v3"
|
||||||
|
|
||||||
|
const blacklistPrefix = "blacklist/"
|
||||||
|
|
||||||
|
func (db *DB) Add(sig string, exp int64) error {
|
||||||
|
return db.store.Set(blacklistPrefix+sig, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Remove(sig string) error {
|
||||||
|
return db.store.Delete(blacklistPrefix + sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Check(sig string) (ok bool) {
|
||||||
|
if db.store.Get(blacklistPrefix+sig, nil) == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Map() (list jwt.BlacklistMap, err error) {
|
||||||
|
list = make(jwt.BlacklistMap)
|
||||||
|
err = db.store.ForEach(func(k string, v []byte) error {
|
||||||
|
if trim, ok := hasTrimPrefix(k, blacklistPrefix); ok {
|
||||||
|
var exp int64
|
||||||
|
err := db.store.Unmarshal(v, &exp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
list[trim] = exp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
63
pkg/db/db.go
Normal file
63
pkg/db/db.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/index"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/store"
|
||||||
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
"io"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
storeFile = "store.db"
|
||||||
|
bcryptCost = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
const blevePath = "bleve"
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
store store.Store
|
||||||
|
Index *index.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg core.Config) (db *DB, err error) {
|
||||||
|
db = new(DB)
|
||||||
|
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||||
|
err = godrop.Unveil(dbFile, "rwc")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.store, err = store.NewBoltStore(dbFile, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
db.Index, err = index.NewIndex(filepath.Join(cfg.DataDir, blevePath))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.UnlockUser()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.RunMigrations()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Dump(w io.Writer) error {
|
||||||
|
return db.store.Dump(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Restore(r io.Reader) error {
|
||||||
|
return db.store.Restore(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Close() (err error) {
|
||||||
|
err = db.store.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return db.Index.Close()
|
||||||
|
}
|
||||||
24
pkg/db/helper.go
Normal file
24
pkg/db/helper.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "crypto/sha256"
|
||||||
|
|
||||||
|
// Parts taken from strings.TrimPrefix
|
||||||
|
func hasTrimPrefix(s, prefix string) (string, bool) {
|
||||||
|
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
|
||||||
|
return s[len(prefix):], true
|
||||||
|
}
|
||||||
|
return s, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePassword(pw string) (b []byte) {
|
||||||
|
b = []byte(pw)
|
||||||
|
if len(pw) <= 56 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write(b)
|
||||||
|
b = hash.Sum(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
48
pkg/db/migration.go
Normal file
48
pkg/db/migration.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
versionKey = "version/version"
|
||||||
|
currentVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type migrator func(*DB) (int, error)
|
||||||
|
|
||||||
|
var migrators = []migrator{
|
||||||
|
func(db *DB) (int, error) {
|
||||||
|
return 0, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) RunMigrations() (err error) {
|
||||||
|
var version int
|
||||||
|
err = db.store.Get(versionKey, &version)
|
||||||
|
if err == store.ErrKeyNotFound {
|
||||||
|
err = db.store.Set(versionKey, currentVersion)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.store.Get(versionKey, &version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("db: database version %d", version)
|
||||||
|
for version < currentVersion {
|
||||||
|
v := version + 1
|
||||||
|
log.Printf("db: running migration %d", v)
|
||||||
|
version, err = migrators[v](db)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.store.Set(versionKey, version)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
169
pkg/db/user.go
Normal file
169
pkg/db/user.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/otp"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userPrefix = "user/"
|
||||||
|
userKey = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errWrongLogin = errors.New("db: wrong login")
|
||||||
|
errLocked = errors.New("db: user locked")
|
||||||
|
errUserExists = errors.New("db: user already exist")
|
||||||
|
|
||||||
|
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *DB) UserExists() (b bool) {
|
||||||
|
db.store.ForEach(func(k string, _ []byte) error {
|
||||||
|
if strings.HasPrefix(k, userPrefix) {
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUserWithoutPassword() (u core.User, err error) {
|
||||||
|
u, err = db.GetUser()
|
||||||
|
u.Password = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUserSecret() (secret string, err error) {
|
||||||
|
var user core.User
|
||||||
|
user, err = db.GetUser()
|
||||||
|
secret = user.Secret
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetUser() (u core.User, err error) {
|
||||||
|
err = db.store.Get(userPrefix+userKey, &u)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) CreateUser(username, password string) error {
|
||||||
|
username = userRe.ReplaceAllString(username, "")
|
||||||
|
if db.UserExists() {
|
||||||
|
return errUserExists
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.store.Set(userPrefix+userKey, &core.User{
|
||||||
|
Username: username,
|
||||||
|
Password: string(hash),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Login(username, password string) (u core.User, err error) {
|
||||||
|
u, err = db.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Username != username {
|
||||||
|
err = errWrongLogin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Locked == 3 {
|
||||||
|
err = errLocked
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||||
|
u.Locked++
|
||||||
|
err = db.store.Set(userPrefix+username, u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errWrongLogin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Locked > 0 {
|
||||||
|
u.Locked = 0
|
||||||
|
err = db.store.Set(userPrefix+userKey, u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Totp(pin string) (u core.User, err error) {
|
||||||
|
u, err = db.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Locked == 3 {
|
||||||
|
err = errLocked
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !otp.Validate(pin, u.Secret) {
|
||||||
|
u.Locked++
|
||||||
|
err = db.store.Set(userPrefix+userKey, u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = errors.New("db: wrong pin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Locked > 0 {
|
||||||
|
u.Locked = 0
|
||||||
|
err = db.store.Set(userPrefix+userKey, u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UnlockUser() (err error) {
|
||||||
|
var u core.User
|
||||||
|
u, err = db.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Locked > 0 {
|
||||||
|
u.Locked = 0
|
||||||
|
err = db.store.Set(userPrefix+userKey, u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateUserSecret(secret string) error {
|
||||||
|
u, err := db.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.Secret = secret
|
||||||
|
return db.store.Set(userPrefix+userKey, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateUserPassword(password string) error {
|
||||||
|
if password == "" {
|
||||||
|
return errors.New("empty password")
|
||||||
|
}
|
||||||
|
u, err := db.GetUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.Password = string(hash)
|
||||||
|
return db.store.Set(userPrefix+userKey, u)
|
||||||
|
}
|
||||||
97
pkg/index/index.go
Normal file
97
pkg/index/index.go
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
"github.com/blevesearch/bleve"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxSearchResult = 101
|
||||||
|
|
||||||
|
type indexDocument struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Index struct {
|
||||||
|
i bleve.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndex(path string) (i *Index, err error) {
|
||||||
|
var bi bleve.Index
|
||||||
|
err = godrop.Unveil(path, "rwc")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||||
|
mapping := bleve.NewIndexMapping()
|
||||||
|
bi, err = bleve.New(path, mapping)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("index: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bi, err = bleve.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("index: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = &Index{i: bi}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Close() error {
|
||||||
|
return i.i.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Add(id, text, docType string) error {
|
||||||
|
doc, _ := i.i.Document(id)
|
||||||
|
if doc != nil {
|
||||||
|
err := i.Delete(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i.i.Index(id, indexDocument{text, core.Now(), docType})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Delete(id string) error {
|
||||||
|
return i.i.Delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Index) Search(search string) (results []core.Result, err error) {
|
||||||
|
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(search))
|
||||||
|
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||||
|
req.Size = maxSearchResult
|
||||||
|
res, err := i.i.Search(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, hit := range res.Hits {
|
||||||
|
if hit == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := core.Result{ID: hit.ID}
|
||||||
|
d, err := i.i.Document(hit.ID)
|
||||||
|
if d == nil || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for fragField, frags := range hit.Fragments {
|
||||||
|
if fragField == "text" || fragField == "type" || fragField == "date" {
|
||||||
|
for _, f := range frags {
|
||||||
|
r.HTML += template.HTML(f)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
59
pkg/log/log.go
Normal file
59
pkg/log/log.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
stdlog "log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
|
||||||
|
debug = false
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitLogger(cfg core.Config) {
|
||||||
|
debug = cfg.Debug
|
||||||
|
if cfg.LogFile == "-" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logfile := filepath.Join(cfg.DataDir, cfg.LogFile)
|
||||||
|
err := godrop.Unveil(logfile, "rwc")
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640)
|
||||||
|
if err != nil {
|
||||||
|
stdlog.Fatal(err)
|
||||||
|
}
|
||||||
|
stdlog.SetOutput(f)
|
||||||
|
log = stdlog.New(f, "", stdlog.LstdFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Println(v ...interface{}) {
|
||||||
|
log.Println(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Printf(fmt string, v ...interface{}) {
|
||||||
|
log.Printf(fmt, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fatal(v ...interface{}) {
|
||||||
|
log.Fatal(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(v ...interface{}) {
|
||||||
|
if debug {
|
||||||
|
log.Println(append([]interface{}{"debug:"}, v...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(fmt string, v ...interface{}) {
|
||||||
|
if debug {
|
||||||
|
log.Printf("debug: "+fmt, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
pkg/otp/otp.go
Normal file
76
pkg/otp/otp.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package otp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
"html/template"
|
||||||
|
"image/png"
|
||||||
|
)
|
||||||
|
|
||||||
|
const size = 200
|
||||||
|
|
||||||
|
func base64Image(key *otp.Key) (data template.URL, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
img, err := key.Image(size, size)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = png.Encode(&buf, img)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Username string
|
||||||
|
Image template.URL
|
||||||
|
Secret string
|
||||||
|
URL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(username string) (req Request, err error) {
|
||||||
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
Issuer: "docstore",
|
||||||
|
AccountName: username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := base64Image(key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return Request{
|
||||||
|
Username: username,
|
||||||
|
Image: data,
|
||||||
|
Secret: key.Secret(),
|
||||||
|
URL: key.URL(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(pin, secret string) bool {
|
||||||
|
if secret == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return totp.Validate(pin, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
||||||
|
key, err := otp.NewKeyFromURL(url)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
secret = key.Secret()
|
||||||
|
data, err = base64Image(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
312
pkg/server/context.go
Normal file
312
pkg/server/context.go
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/jwt/v3"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userClaim = "user"
|
||||||
|
totpClaim = "totp"
|
||||||
|
rememberClaim = "remember"
|
||||||
|
refererClaim = "referer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) {
|
||||||
|
h := w.Header()
|
||||||
|
h.Set("X-Frame-Options", "DENY")
|
||||||
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
h.Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'")
|
||||||
|
h.Set("Referrer-Policy", "same-origin")
|
||||||
|
ctx = &Context{
|
||||||
|
Request: r,
|
||||||
|
Response: w,
|
||||||
|
Srv: s,
|
||||||
|
Time: time.Now(),
|
||||||
|
}
|
||||||
|
path := ctx.Path()
|
||||||
|
if path == "/bootstrap.css" || path == "/custom.css" || path == "/favicon.ico" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := r.Cookie(cookieName); err == nil {
|
||||||
|
t, err := jwt.DecodeToken(c.Value)
|
||||||
|
if err != nil {
|
||||||
|
ctx.LogSetCookie("DecodeToken:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = s.JWT.Verify(t); err != nil {
|
||||||
|
ctx.LogSetCookie("VerifyToken:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t.Claims.GetString(totpClaim) != "" {
|
||||||
|
if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
|
||||||
|
ctx.Token = *t
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
if err = s.JWT.Invalidate(t); err != nil {
|
||||||
|
log.Println("Invalidate:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.SetCookie(nil, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Request *http.Request
|
||||||
|
Response http.ResponseWriter
|
||||||
|
Srv *HTTPServer
|
||||||
|
T *template.Template
|
||||||
|
Status int
|
||||||
|
Err error
|
||||||
|
Time time.Time
|
||||||
|
|
||||||
|
Token jwt.Token
|
||||||
|
|
||||||
|
Data webData
|
||||||
|
}
|
||||||
|
|
||||||
|
type webData struct {
|
||||||
|
Version string
|
||||||
|
Time int64
|
||||||
|
|
||||||
|
Title string
|
||||||
|
BodyTitle string
|
||||||
|
|
||||||
|
Login bool
|
||||||
|
|
||||||
|
Key string
|
||||||
|
|
||||||
|
User string
|
||||||
|
Token string
|
||||||
|
Msg string
|
||||||
|
Search string
|
||||||
|
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loginData struct {
|
||||||
|
User string
|
||||||
|
Referer string
|
||||||
|
}
|
||||||
|
|
||||||
|
type sectionData struct {
|
||||||
|
Name string
|
||||||
|
Members map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type pageData struct {
|
||||||
|
Page core.Page
|
||||||
|
Sections map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Exec() {
|
||||||
|
defer c.log()
|
||||||
|
if c.T == nil {
|
||||||
|
c.Status = http.StatusInternalServerError
|
||||||
|
c.Err = errors.New("template is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
|
||||||
|
|
||||||
|
c.Data.Version = c.Srv.Config.Version
|
||||||
|
if c.Data.BodyTitle == "" {
|
||||||
|
c.Data.BodyTitle = c.Data.Title
|
||||||
|
}
|
||||||
|
c.Data.User = c.User()
|
||||||
|
c.Data.Login = c.LoggedOn()
|
||||||
|
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
|
||||||
|
|
||||||
|
if c.Data.Msg != "" {
|
||||||
|
c.Status = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Err = c.T.Execute(c.Response, c.Data)
|
||||||
|
if c.Err != nil {
|
||||||
|
c.Status = http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
if c.Status == 0 {
|
||||||
|
c.Status = http.StatusOK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) log() {
|
||||||
|
if c.Err != nil {
|
||||||
|
log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Error(i interface{}) {
|
||||||
|
switch v := i.(type) {
|
||||||
|
case string:
|
||||||
|
c.Data.Msg = v
|
||||||
|
case error:
|
||||||
|
c.Data.Msg = v.Error()
|
||||||
|
}
|
||||||
|
c.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) NotFound() {
|
||||||
|
c.Template("notFoundHandler")
|
||||||
|
c.Data = webData{Title: "404"}
|
||||||
|
c.Status = http.StatusNotFound
|
||||||
|
c.Response.WriteHeader(http.StatusNotFound)
|
||||||
|
c.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Forbidden() {
|
||||||
|
c.Template("forbiddenHandler")
|
||||||
|
c.Data = webData{Title: "403"}
|
||||||
|
c.Status = http.StatusForbidden
|
||||||
|
c.Response.WriteHeader(http.StatusForbidden)
|
||||||
|
c.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SwitchHandler() {
|
||||||
|
c.Srv.srv.Handler = c.Srv.handler
|
||||||
|
c.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Template(name string) {
|
||||||
|
c.T = c.Srv.templ[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Write(buf []byte) (err error) {
|
||||||
|
_, err = c.Response.Write(buf)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetHeader(name, value string) {
|
||||||
|
c.Response.Header().Set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Redirect(uri string, code int) {
|
||||||
|
if len(uri) > 0 && uri[0] != '/' {
|
||||||
|
uri = "/" + uri
|
||||||
|
}
|
||||||
|
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
|
||||||
|
http.Redirect(c.Response, c.Request, uri, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Method() string {
|
||||||
|
return c.Request.Method
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Path() string {
|
||||||
|
return c.Request.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) RefererURI() string {
|
||||||
|
u, err := url.Parse(c.Request.Header.Get("Referer"))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return u.RequestURI()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) FormSlice(name string) []string {
|
||||||
|
// does nothing if called twice
|
||||||
|
c.Request.ParseForm()
|
||||||
|
return c.Request.PostForm[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Form(name string) string {
|
||||||
|
return c.Request.PostFormValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Var(name string) (ret string) {
|
||||||
|
ret, _ = mux.Vars(c.Request)[name]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckXsrf validates the xsrf token
|
||||||
|
func (c *Context) CheckXsrf() (ok bool) {
|
||||||
|
ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
|
||||||
|
if !ok {
|
||||||
|
c.Error("wrong csrf token")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) LoggedOn() (ok bool) {
|
||||||
|
_, ok = c.Token.Claims.Get(userClaim)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) LogSetCookie(msg string, err error) {
|
||||||
|
log.Printf("ctx: %s %s\n", msg, err)
|
||||||
|
c.SetCookie(nil, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Login(u core.User, remember string) {
|
||||||
|
var d time.Duration
|
||||||
|
if remember != "" {
|
||||||
|
d = day * 7
|
||||||
|
}
|
||||||
|
c.SetCookie(jwt.Claims{userClaim: u.Username}, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetCookie(claims jwt.Claims, d time.Duration) {
|
||||||
|
c.setCookie(jwt.NewToken(claims, nil), d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) setCookie(t *jwt.Token, d time.Duration) {
|
||||||
|
if d == 0 {
|
||||||
|
d = jwt.DefaultExpiry
|
||||||
|
}
|
||||||
|
t.Claims[jwt.ExpClaim] = jwt.NewExp(d)
|
||||||
|
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Token = *t
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: cookieName,
|
||||||
|
Value: c.Token.String(),
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: int(d.Seconds()),
|
||||||
|
Secure: c.Srv.Config.SecureCookie,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
}
|
||||||
|
if cookie.MaxAge > 0 {
|
||||||
|
cookie.Expires = time.Now().Add(d)
|
||||||
|
} else if cookie.MaxAge < 0 {
|
||||||
|
cookie.Expires = time.Unix(1, 0)
|
||||||
|
}
|
||||||
|
http.SetCookie(c.Response, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) User() string {
|
||||||
|
return c.Token.Claims.GetString(userClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Totp() string {
|
||||||
|
return c.Token.Claims.GetString(totpClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Remember() string {
|
||||||
|
return c.Token.Claims.GetString(rememberClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Referer() string {
|
||||||
|
return c.Token.Claims.GetString(refererClaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ctxHandler func(*Context)
|
||||||
864
pkg/server/handler.go
Normal file
864
pkg/server/handler.go
Normal file
|
|
@ -0,0 +1,864 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/otp"
|
||||||
|
"git.giftfish.de/ston1th/jwt/v3"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type notFoundHandler struct {
|
||||||
|
s *HTTPServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
newContext(w, r, nf.s).NotFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
func totpAuthHandler(h ctxHandler) ctxHandler {
|
||||||
|
return func(ctx *Context) {
|
||||||
|
if ctx.Totp() != "" {
|
||||||
|
h(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authHandler(h ctxHandler) ctxHandler {
|
||||||
|
return func(ctx *Context) {
|
||||||
|
if ctx.LoggedOn() {
|
||||||
|
h(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Forbidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticHandler(ctx *Context) {
|
||||||
|
var err error
|
||||||
|
ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
|
||||||
|
ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat))
|
||||||
|
switch ctx.Path() {
|
||||||
|
case "/bootstrap.css":
|
||||||
|
err = ctx.Write(ctx.Srv.res["bootstrap.css"])
|
||||||
|
case "/custom.css":
|
||||||
|
err = ctx.Write(ctx.Srv.res["custom.css"])
|
||||||
|
case "/favicon.ico":
|
||||||
|
ctx.SetHeader("Content-Type", "image/x-icon")
|
||||||
|
err = ctx.Write(ctx.Srv.res["favicon.ico"])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Println("static:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type registerNotFoundHandler struct {
|
||||||
|
s *HTTPServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nf *registerNotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
newContext(w, r, nf.s).Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerHandler(ctx *Context) {
|
||||||
|
ctx.Template("registerHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Register User",
|
||||||
|
BodyTitle: "Register User",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ctx.Form("user")
|
||||||
|
password := ctx.Form("password")
|
||||||
|
repeat := ctx.Form("repeat")
|
||||||
|
|
||||||
|
if user == "" || password == "" {
|
||||||
|
ctx.Error("empty user or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if password != repeat {
|
||||||
|
ctx.Error("passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Srv.DB.CreateUser(user, password); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SwitchHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexHandler(ctx *Context) {
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginHandler(ctx *Context) {
|
||||||
|
if ctx.LoggedOn() {
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Template("loginHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Login",
|
||||||
|
BodyTitle: "Login",
|
||||||
|
}
|
||||||
|
var data loginData
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
data.Referer = ctx.RefererURI()
|
||||||
|
ctx.Data.Data = data
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.User = ctx.Form("user")
|
||||||
|
data.Referer = ctx.Form("referer")
|
||||||
|
ctx.Data.Data = data
|
||||||
|
if data.Referer == "" {
|
||||||
|
data.Referer = core.IndexURI
|
||||||
|
}
|
||||||
|
password := ctx.Form("password")
|
||||||
|
if data.User == "" || password == "" {
|
||||||
|
ctx.Error("empty username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u, err := ctx.Srv.DB.Login(data.User, password)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("login:", data.User, err)
|
||||||
|
ctx.Error("bad username or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Secret == "" {
|
||||||
|
log.Println("login:", u.Username)
|
||||||
|
ctx.Login(u, ctx.Form("remember"))
|
||||||
|
ctx.Redirect(data.Referer, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.SetCookie(jwt.Claims{
|
||||||
|
totpClaim: u.Username,
|
||||||
|
rememberClaim: ctx.Form("remember"),
|
||||||
|
refererClaim: data.Referer,
|
||||||
|
}, time.Minute)
|
||||||
|
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginTotpHandler(ctx *Context) {
|
||||||
|
if ctx.LoggedOn() {
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Template("loginTotpHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "TOTP Verification",
|
||||||
|
BodyTitle: "TOTP Veriftcation",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin := ctx.Form("pin")
|
||||||
|
if pin == "" {
|
||||||
|
ctx.Error("empty pin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u, err := ctx.Srv.DB.Totp(pin)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ref := ctx.Referer()
|
||||||
|
log.Println("login:", u.Username)
|
||||||
|
ctx.Login(u, ctx.Remember())
|
||||||
|
ctx.Redirect(ref, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchHandler(ctx *Context) {
|
||||||
|
ctx.Template("searchHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Search",
|
||||||
|
BodyTitle: "Search",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
search := ctx.Form("search")
|
||||||
|
|
||||||
|
res, err := ctx.Srv.DB.Index.Search(search)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ctx.User()
|
||||||
|
r := make([]core.Result, len(res))
|
||||||
|
i := 0
|
||||||
|
for _, v := range res {
|
||||||
|
if ctx.Srv.DB.ValidatePage(v.StoreTitle, user) {
|
||||||
|
r[i] = v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data.Data = r[:i]
|
||||||
|
ctx.Data.Search = search
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionsHandler(ctx *Context) {
|
||||||
|
ctx.Template("sectionsHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "All Sections",
|
||||||
|
BodyTitle: "All Sections",
|
||||||
|
Admin: ctx.Admin(),
|
||||||
|
}
|
||||||
|
ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin())
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionHandler(ctx *Context) {
|
||||||
|
ctx.Template("sectionHandler")
|
||||||
|
section := ctx.Var("section")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: section,
|
||||||
|
BodyTitle: section,
|
||||||
|
Admin: ctx.Admin(),
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin())
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionNewHandler(ctx *Context) {
|
||||||
|
ctx.Template("sectionNewHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "New Section",
|
||||||
|
BodyTitle: "New Section",
|
||||||
|
}
|
||||||
|
users, err := ctx.Srv.DB.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sd := sectionData{Members: make(map[string]bool)}
|
||||||
|
for _, u := range users {
|
||||||
|
sd.Members[u.Username] = false
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Data.Data = sd
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
section := ctx.Form("section")
|
||||||
|
members := ctx.FormSlice("members")
|
||||||
|
for _, m := range members {
|
||||||
|
sd.Members[m] = true
|
||||||
|
}
|
||||||
|
ctx.Data.Data = sd
|
||||||
|
if section == "" {
|
||||||
|
ctx.Error("empty section")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ctx.Srv.DB.CreateSection(section, members, false)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("create: section %s created by %s\n", section, ctx.User())
|
||||||
|
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionEditHandler(ctx *Context) {
|
||||||
|
ctx.Template("sectionEditHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Edit Section",
|
||||||
|
BodyTitle: "Edit Section",
|
||||||
|
}
|
||||||
|
users, err := ctx.Srv.DB.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sd := sectionData{Members: make(map[string]bool)}
|
||||||
|
for _, u := range users {
|
||||||
|
sd.Members[u.Username] = false
|
||||||
|
}
|
||||||
|
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sd.Name = s.Name
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
for _, m := range s.Members {
|
||||||
|
sd.Members[m] = true
|
||||||
|
}
|
||||||
|
ctx.Data.Data = sd
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
members := ctx.FormSlice("members")
|
||||||
|
for _, m := range members {
|
||||||
|
sd.Members[m] = true
|
||||||
|
}
|
||||||
|
ctx.Data.Data = sd
|
||||||
|
err = ctx.Srv.DB.UpdateSection(s.Name, members, s.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update: section %s updated by %s\n", s.Name, ctx.User())
|
||||||
|
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionDelHandler(ctx *Context) {
|
||||||
|
ctx.Template("sectionDelHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Delete Section",
|
||||||
|
BodyTitle: "Delete Section",
|
||||||
|
}
|
||||||
|
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.User {
|
||||||
|
ctx.Error("user section can not be deleted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Data.Data = s.Name
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := ctx.Srv.DB.DeleteSection(s.Name)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("delete: section %s deleted by %s\n", s.Name, ctx.User())
|
||||||
|
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageNewHandler(ctx *Context) {
|
||||||
|
ctx.Template("pageNewHandler")
|
||||||
|
ctx.Data = webData{Title: "New Page"}
|
||||||
|
user := ctx.User()
|
||||||
|
secs, err := ctx.Srv.DB.GetUserSections(user)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
secs[user] = true
|
||||||
|
ctx.Data.Data = pageData{core.Page{Perm: core.Public}, secs}
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
section := ctx.Form("section")
|
||||||
|
title := ctx.Form("title")
|
||||||
|
markdown := ctx.Form("markdown")
|
||||||
|
perm := core.ParsePermString(ctx.Form("perm"))
|
||||||
|
p := core.Page{
|
||||||
|
Title: title,
|
||||||
|
Markdown: markdown,
|
||||||
|
Perm: perm,
|
||||||
|
}
|
||||||
|
secs[section] = true
|
||||||
|
ctx.Data.Data = pageData{p, secs}
|
||||||
|
if title == "" {
|
||||||
|
ctx.Error("empty title")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if perm == core.Invalid {
|
||||||
|
ctx.Error("invalid permission")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, user, perm)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageHandler(ctx *Context) {
|
||||||
|
var pageCreated string
|
||||||
|
ctx.Data = webData{
|
||||||
|
Admin: ctx.Admin(),
|
||||||
|
}
|
||||||
|
section := ctx.Var("section")
|
||||||
|
title := ctx.Var("title")
|
||||||
|
user := ctx.User()
|
||||||
|
key := ctx.Var("key")
|
||||||
|
if key != "" {
|
||||||
|
t, err := jwt.DecodeToken(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("share: DecodeToken:", err)
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||||
|
log.Println("share: VerifyToken:", err)
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user = t.Claims.GetString(sharedClaim)
|
||||||
|
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(userCreatedClaim)) {
|
||||||
|
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||||
|
log.Println("share: Invalidate:", err)
|
||||||
|
}
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
section = t.Claims.GetString(sectionClaim)
|
||||||
|
title = t.Claims.GetString(titleClaim)
|
||||||
|
pageCreated = t.Claims.GetString(pageCreatedClaim)
|
||||||
|
ctx.Data.Key = key
|
||||||
|
if ctx.T == nil {
|
||||||
|
ctx.Template("pageViewHandler")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ctx.T == nil {
|
||||||
|
ctx.Template("pageHandler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if key != "" && page.Created != pageCreated {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = page
|
||||||
|
ctx.Data.Title = page.Title
|
||||||
|
ctx.Data.BodyTitle = page.Title
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageMDHandler(f ctxHandler) ctxHandler {
|
||||||
|
return func(ctx *Context) {
|
||||||
|
ctx.Template("pageMDHandler")
|
||||||
|
f(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageShareHandler(ctx *Context) {
|
||||||
|
ctx.Template("pageShareHandler")
|
||||||
|
ctx.Data = webData{}
|
||||||
|
section := ctx.Var("section")
|
||||||
|
title := ctx.Var("title")
|
||||||
|
user := ctx.User()
|
||||||
|
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if page.Perm != core.Private {
|
||||||
|
ctx.Error("only private pages can be shared")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = page.StoreTitle
|
||||||
|
ctx.Data.Title = page.Title
|
||||||
|
ctx.Data.BodyTitle = page.Title
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := duration(ctx.Form("duration"), ctx.Form("mode"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t := jwt.NewToken(map[string]interface{}{
|
||||||
|
sharedClaim: user,
|
||||||
|
userCreatedClaim: ctx.Token.Claims.GetString(userCreatedClaim),
|
||||||
|
pageCreatedClaim: page.Created,
|
||||||
|
sectionClaim: section,
|
||||||
|
titleClaim: title,
|
||||||
|
jwt.ExpClaim: jwt.NewExp(d),
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if err = ctx.Srv.JWT.Sign(t); err != nil {
|
||||||
|
log.Println("share: SignToken:", err)
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Key = t.String()
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageBlacklistHandler(ctx *Context) {
|
||||||
|
ctx.Template("pageBlacklistHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Blacklist",
|
||||||
|
BodyTitle: "Blacklist",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := jwt.DecodeToken(ctx.Form("share"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ctx.Srv.JWT.Verify(t)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := t.Claims.GetString(sharedClaim)
|
||||||
|
if user == "" {
|
||||||
|
ctx.Error("blacklisting denied: no share token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.User() != user {
|
||||||
|
ctx.Error("blacklisting denied: token was issued by " + user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = ctx.Srv.JWT.Invalidate(t)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageEditHandler(ctx *Context) {
|
||||||
|
ctx.Template("pageEditHandler")
|
||||||
|
ctx.Data = webData{}
|
||||||
|
section := ctx.Var("section")
|
||||||
|
title := ctx.Var("title")
|
||||||
|
page, err := ctx.Srv.DB.GetPage(section, title, ctx.User())
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Title = page.Title
|
||||||
|
ctx.Data.BodyTitle = page.Title
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Data.Data = page
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.Markdown = ctx.Form("markdown")
|
||||||
|
page.Perm = core.ParsePermString(ctx.Form("perm"))
|
||||||
|
ctx.Data.Data = page
|
||||||
|
|
||||||
|
err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pageDelHandler(ctx *Context) {
|
||||||
|
section := ctx.Var("section")
|
||||||
|
title := ctx.Var("title")
|
||||||
|
ctx.Template("pageDelHandler")
|
||||||
|
ctx.Data.Title = title
|
||||||
|
ctx.Data.BodyTitle = title
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usersHandler(ctx *Context) {
|
||||||
|
if !ctx.LoggedOn() {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Template("usersHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Users",
|
||||||
|
BodyTitle: "Users",
|
||||||
|
User: ctx.User(),
|
||||||
|
}
|
||||||
|
us, err := ctx.Srv.DB.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Data.Msg = err.Error()
|
||||||
|
}
|
||||||
|
ctx.Data.Data = us
|
||||||
|
ctx.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userNewHandler(ctx *Context) {
|
||||||
|
ctx.Template("userNewHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "New User",
|
||||||
|
BodyTitle: "New User",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ctx.Form("user")
|
||||||
|
password := ctx.Form("password")
|
||||||
|
repeat := ctx.Form("repeat")
|
||||||
|
admin := ctx.Form("admin")
|
||||||
|
adm := false
|
||||||
|
if admin == "0" {
|
||||||
|
adm = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == "" || password == "" {
|
||||||
|
ctx.Error("empty user or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if password != repeat {
|
||||||
|
ctx.Error("passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("create: user %s created by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect("/users", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userEditHandler(ctx *Context) {
|
||||||
|
ctx.Template("userEditHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Edit User",
|
||||||
|
BodyTitle: "Edit User",
|
||||||
|
}
|
||||||
|
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = u
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := ctx.Var("user")
|
||||||
|
password := ctx.Form("password")
|
||||||
|
repeat := ctx.Form("repeat")
|
||||||
|
admin := ctx.Form("admin")
|
||||||
|
adm := false
|
||||||
|
if admin == "0" {
|
||||||
|
adm = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if password != repeat {
|
||||||
|
ctx.Error("passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Admin() {
|
||||||
|
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update: user %s updated by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect("/users", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update: %s updated by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userTotpHandler(ctx *Context) {
|
||||||
|
ctx.Template("userTotpHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "TOTP",
|
||||||
|
BodyTitle: "TOTP",
|
||||||
|
}
|
||||||
|
req := otp.Request{Username: ctx.Var("user")}
|
||||||
|
u, err := ctx.Srv.DB.GetUser(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if u.Secret == "" {
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
req, err = otp.New(req.Username)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "POST":
|
||||||
|
req.URL = ctx.Form("url")
|
||||||
|
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
ctx.Data.Data = req
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pin := ctx.Form("pin")
|
||||||
|
if pin == "" {
|
||||||
|
ctx.Error("empty pin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = req
|
||||||
|
secret := req.Secret
|
||||||
|
if u.Secret != "" {
|
||||||
|
secret = u.Secret
|
||||||
|
}
|
||||||
|
valid := otp.Validate(pin, secret)
|
||||||
|
user := ctx.User()
|
||||||
|
if user != req.Username && ctx.Admin() {
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
ctx.Error("validation failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Secret == "" {
|
||||||
|
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
|
||||||
|
} else {
|
||||||
|
log.Printf("totp: enabled by %s for %s\n", user, req.Username)
|
||||||
|
}
|
||||||
|
ctx.Redirect("/user/edit/"+req.Username, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userUnlockHandler(ctx *Context) {
|
||||||
|
user := ctx.Var("user")
|
||||||
|
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
||||||
|
log.Println("unlock:", err)
|
||||||
|
}
|
||||||
|
log.Printf("unlock: %s unlocked by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect("/users", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userDelHandler(ctx *Context) {
|
||||||
|
ctx.Template("userDelHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Delete User",
|
||||||
|
BodyTitle: "Delete User",
|
||||||
|
}
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = user
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self := ctx.User()
|
||||||
|
user := ctx.Var("user")
|
||||||
|
if err := ctx.Srv.DB.DeleteUser(user); err != nil {
|
||||||
|
log.Println("delete:", err)
|
||||||
|
}
|
||||||
|
log.Printf("delete: user %s deleted by %s\n", user, self)
|
||||||
|
if user == self {
|
||||||
|
ctx.Redirect("/logout", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect("/users", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logoutHandler(ctx *Context) {
|
||||||
|
user := ctx.User()
|
||||||
|
if user == "" {
|
||||||
|
user = ctx.Totp() + " (totp)"
|
||||||
|
}
|
||||||
|
log.Println("logout:", user)
|
||||||
|
ctx.Srv.JWT.Invalidate(&ctx.Token)
|
||||||
|
ctx.SetCookie(nil, 0)
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
|
}
|
||||||
45
pkg/server/helper.go
Normal file
45
pkg/server/helper.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
day = time.Hour * 24
|
||||||
|
min = time.Second * 30
|
||||||
|
max = day * 90
|
||||||
|
)
|
||||||
|
|
||||||
|
func duration(duration, mode string) (d time.Duration, err error) {
|
||||||
|
switch mode {
|
||||||
|
case "1":
|
||||||
|
d, err = time.ParseDuration(duration)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("could not parse duration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "2":
|
||||||
|
var t int
|
||||||
|
t, err = strconv.Atoi(duration)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("could not parse duration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d = time.Duration(int(day) * t)
|
||||||
|
default:
|
||||||
|
err = errors.New("invalid mode")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if d < min {
|
||||||
|
err = errors.New("minimum duration is 30 seconds")
|
||||||
|
}
|
||||||
|
if d > max {
|
||||||
|
err = errors.New("maximum duration is 90 days")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
99
pkg/server/routes.go
Normal file
99
pkg/server/routes.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
type route struct {
|
||||||
|
Path string
|
||||||
|
Handler ctxHandler
|
||||||
|
Methods []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var static = []route{
|
||||||
|
{
|
||||||
|
"/favicon.ico",
|
||||||
|
staticHandler,
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/bootstrap.css",
|
||||||
|
staticHandler,
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/custom.css",
|
||||||
|
staticHandler,
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var register = append(static, route{
|
||||||
|
"/",
|
||||||
|
registerHandler,
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
})
|
||||||
|
|
||||||
|
var routes = append(static, []route{
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
authHandler(
|
||||||
|
dirHandler),
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/sections",
|
||||||
|
sectionsHandler,
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/search",
|
||||||
|
authHandler(
|
||||||
|
searchHandler),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/upload",
|
||||||
|
authHandler(
|
||||||
|
searchHandler),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/login",
|
||||||
|
loginHandler,
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/totp",
|
||||||
|
totpAuthHandler(
|
||||||
|
loginTotpHandler),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/user/edit/{user:[a-zA-Z0-9]+$}",
|
||||||
|
authHandler(
|
||||||
|
userEditHandler),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/user/totp/{user:[a-zA-Z0-9]+$}",
|
||||||
|
authHandler(
|
||||||
|
userTotpHandler),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/logout",
|
||||||
|
logoutHandler,
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/{dir}",
|
||||||
|
authHandler(
|
||||||
|
dirHandler),
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/{dir}/{file}",
|
||||||
|
authHandler(
|
||||||
|
fileHandler),
|
||||||
|
[]string{"GET"},
|
||||||
|
},
|
||||||
|
}...)
|
||||||
112
pkg/server/server.go
Normal file
112
pkg/server/server.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/db"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
"git.giftfish.de/ston1th/jwt/v3"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"html/template"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cookieName = "gosession"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPServer struct {
|
||||||
|
Config core.Config
|
||||||
|
|
||||||
|
listener net.Listener
|
||||||
|
srv *http.Server
|
||||||
|
handler http.Handler
|
||||||
|
|
||||||
|
DB *db.DB
|
||||||
|
JWT *jwt.JWT
|
||||||
|
|
||||||
|
templ map[string]*template.Template
|
||||||
|
res map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
|
||||||
|
srv = &HTTPServer{
|
||||||
|
Config: cfg,
|
||||||
|
listener: l,
|
||||||
|
handler: srv.buildRoutes(),
|
||||||
|
}
|
||||||
|
srv.loadTemplates()
|
||||||
|
srv.srv = &http.Server{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) Start() (err error) {
|
||||||
|
s.DB, err = db.New(s.Config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("db: " + err.Error())
|
||||||
|
}
|
||||||
|
err = godrop.UnveilBlock()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.Config.Secret != "" {
|
||||||
|
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Config.Secret))
|
||||||
|
} else {
|
||||||
|
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("server: " + err.Error())
|
||||||
|
}
|
||||||
|
if !s.DB.UserExists() {
|
||||||
|
s.srv.Handler = s.register()
|
||||||
|
} else {
|
||||||
|
s.srv.Handler = s.handler
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
log.Println(s.srv.Serve(s.listener))
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) Stop() error {
|
||||||
|
s.DB.Close()
|
||||||
|
s.JWT.Stop()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
return s.srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) register() http.Handler {
|
||||||
|
m := mux.NewRouter()
|
||||||
|
m.NotFoundHandler = ®isterNotFoundHandler{s}
|
||||||
|
for _, v := range register {
|
||||||
|
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) buildRoutes() http.Handler {
|
||||||
|
m := mux.NewRouter()
|
||||||
|
m.NotFoundHandler = ¬FoundHandler{s}
|
||||||
|
for _, v := range routes {
|
||||||
|
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(w, r, s)
|
||||||
|
if ctx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
1039
pkg/server/templates.go
Normal file
1039
pkg/server/templates.go
Normal file
File diff suppressed because one or more lines are too long
49
pkg/server/xsrf.go
Normal file
49
pkg/server/xsrf.go
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/jwt/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keySize = jwt.KeySize / 2
|
||||||
|
|
||||||
|
func genKey(length int) (bytes []byte) {
|
||||||
|
bytes = make([]byte, length)
|
||||||
|
if _, err := rand.Reader.Read(bytes); err != nil {
|
||||||
|
log.Println("genKey:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func newXsrf(secret []byte) string {
|
||||||
|
rnd := genKey(keySize)
|
||||||
|
return base64.RawURLEncoding.EncodeToString(append(rnd, xor(rnd, secret)...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkXsrf(xsrf string, secret []byte) bool {
|
||||||
|
t, err := base64.RawURLEncoding.DecodeString(xsrf)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(t) != jwt.KeySize {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func xor(a, b []byte) (c []byte) {
|
||||||
|
n := len(a)
|
||||||
|
if len(b) < n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c = make([]byte, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
c[i] = a[i] ^ b[i]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
31
pkg/server/xsrf_test.go
Normal file
31
pkg/server/xsrf_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestXsrf(t *testing.T) {
|
||||||
|
sec := genKey(keySize)
|
||||||
|
if len(sec) != keySize {
|
||||||
|
t.Fatal("len(sec) != keySize")
|
||||||
|
}
|
||||||
|
xsrf := newXsrf(sec)
|
||||||
|
if xsrf == "" {
|
||||||
|
t.Fatal("xsrf is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !checkXsrf(xsrf, sec) {
|
||||||
|
t.Fatal("valid xsrf check failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkXsrf("test", sec) {
|
||||||
|
t.Fatal("invalid xsrf check succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
rnd := genKey(keySize)
|
||||||
|
if checkXsrf(xsrf, rnd) {
|
||||||
|
t.Fatal("invalid xsrf check succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
130
pkg/store/boltstore.go
Normal file
130
pkg/store/boltstore.go
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultBoltBucket = "default"
|
||||||
|
fileMode = 0640
|
||||||
|
)
|
||||||
|
|
||||||
|
type BoltStore struct {
|
||||||
|
Marshaler
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBoltStore(file string, m Marshaler) (Store, error) {
|
||||||
|
if m == nil {
|
||||||
|
m = NewGOB()
|
||||||
|
}
|
||||||
|
db, err := bolt.Open(file, fileMode, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = db.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
_, err = tx.CreateBucketIfNotExists([]byte(defaultBoltBucket))
|
||||||
|
return
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BoltStore{m, db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dump struct {
|
||||||
|
K string `json:"k"`
|
||||||
|
V []byte `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Dump(w io.Writer) (err error) {
|
||||||
|
if w == nil {
|
||||||
|
return ErrWriterIsNil
|
||||||
|
}
|
||||||
|
var d []dump
|
||||||
|
err = bs.ForEach(func(k string, v []byte) error {
|
||||||
|
b := make([]byte, len(v))
|
||||||
|
copy(b, v)
|
||||||
|
d = append(d, dump{k, b})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return json.NewEncoder(w).Encode(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Restore(r io.Reader) (err error) {
|
||||||
|
if r == nil {
|
||||||
|
return ErrReaderIsNil
|
||||||
|
}
|
||||||
|
var d []dump
|
||||||
|
err = json.NewDecoder(r).Decode(&d)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
if err = tx.DeleteBucket([]byte(defaultBoltBucket)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = tx.CreateBucket([]byte(defaultBoltBucket))
|
||||||
|
return
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
b := tx.Bucket([]byte(defaultBoltBucket))
|
||||||
|
for _, v := range d {
|
||||||
|
b.Put([]byte(v.K), v.V)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Get(key string, v interface{}) (err error) {
|
||||||
|
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte(defaultBoltBucket)).Get([]byte(key))
|
||||||
|
if b == nil {
|
||||||
|
return ErrKeyNotFound
|
||||||
|
}
|
||||||
|
if v != nil {
|
||||||
|
return bs.Unmarshal(b, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Set(key string, v interface{}) error {
|
||||||
|
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||||
|
var b []byte
|
||||||
|
if v != nil {
|
||||||
|
b, err = bs.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Bucket([]byte(defaultBoltBucket)).Put([]byte(key), b)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
|
||||||
|
return bs.db.View(func(tx *bolt.Tx) error {
|
||||||
|
return tx.Bucket([]byte(defaultBoltBucket)).ForEach(func(k, v []byte) error {
|
||||||
|
return f(string(k), v)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Delete(key string) error {
|
||||||
|
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BoltStore) Close() error {
|
||||||
|
return bs.db.Close()
|
||||||
|
}
|
||||||
99
pkg/store/boltstore_test.go
Normal file
99
pkg/store/boltstore_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testData struct {
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoltStore(t *testing.T) {
|
||||||
|
data := []testData{
|
||||||
|
{"123"},
|
||||||
|
{"hello"},
|
||||||
|
}
|
||||||
|
dir, err := ioutil.TempDir("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
bs, err := NewBoltStore(filepath.Join(dir, "bolt.db"), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bs.Set("key0", data[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = bs.Set("key1", data[1])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("GetKey0", func(t *testing.T) {
|
||||||
|
var d testData
|
||||||
|
err := bs.Get("key0", &d)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if d != data[0] {
|
||||||
|
t.Error("d is not data[0]")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("ForEach", func(t *testing.T) {
|
||||||
|
err := bs.ForEach(func(k string, v []byte) error {
|
||||||
|
if k == "key0" || k == "key1" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("key0 or key1 not found")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("DumpRestore", func(t *testing.T) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err := bs.Dump(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
t.Error("no dump written")
|
||||||
|
}
|
||||||
|
err = bs.Restore(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("DeleteKey1", func(t *testing.T) {
|
||||||
|
err := bs.Delete("key1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("GetKey1", func(t *testing.T) {
|
||||||
|
var d testData
|
||||||
|
err := bs.Get("key1", &d)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("key1 should be deleted")
|
||||||
|
}
|
||||||
|
if d == data[1] {
|
||||||
|
t.Error("d is not data[1]")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("Close", func(t *testing.T) {
|
||||||
|
err := bs.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
13
pkg/store/dump.go
Normal file
13
pkg/store/dump.go
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type Dumper interface {
|
||||||
|
Dump(io.Writer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Restorer interface {
|
||||||
|
Restore(io.Reader) error
|
||||||
|
}
|
||||||
30
pkg/store/marshal.go
Normal file
30
pkg/store/marshal.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Marshaler interface {
|
||||||
|
Marshal(v interface{}) ([]byte, error)
|
||||||
|
Unmarshal(data []byte, v interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type gobMarshaler struct{}
|
||||||
|
|
||||||
|
func NewGOB() Marshaler {
|
||||||
|
return gobMarshaler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = gob.NewEncoder(buf).Encode(v)
|
||||||
|
b = buf.Bytes()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||||
|
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||||
|
}
|
||||||
22
pkg/store/store.go
Normal file
22
pkg/store/store.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrKeyNotFound = errors.New("store: key not found")
|
||||||
|
ErrWriterIsNil = errors.New("store: writer is nil")
|
||||||
|
ErrReaderIsNil = errors.New("store: reader is nil")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Marshaler
|
||||||
|
Dumper
|
||||||
|
Restorer
|
||||||
|
Get(string, interface{}) error
|
||||||
|
Set(string, interface{}) error
|
||||||
|
ForEach(func(string, []byte) error) error
|
||||||
|
Delete(string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
60
pkg/tesseract/tesseract.go
Normal file
60
pkg/tesseract/tesseract.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package tesseract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tesseract struct {
|
||||||
|
cmd string
|
||||||
|
args []string
|
||||||
|
dir string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTesseract(dir string, langs []string, timeout time.Duration) (t *Tesseract, err error) {
|
||||||
|
var (
|
||||||
|
cmd string
|
||||||
|
lang string
|
||||||
|
)
|
||||||
|
cmd, err = exec.LookPath("tesseract")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, l := range langs {
|
||||||
|
lang += l
|
||||||
|
if i < len(langs) {
|
||||||
|
lang += "+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args := []string{"-l", lang, "--oem", "2"}
|
||||||
|
t = &Tesseract{cmd, args, dir, timeout}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tesseract) Scan(file string) (filename, text string, err error) {
|
||||||
|
filename = core.FileName() + ".pdf"
|
||||||
|
txt, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{file, "stdout", "txt"}...)...), t.timeout)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text = string(txt)
|
||||||
|
pdf, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{file, "stdout", "pdf"}...)...), t.timeout)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(filepath.Join(t.dir, filename), os.O_RDWR|os.O_CREATE, 0640)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = f.Write(pdf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.Remove(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
55
scripts/gowiki_init
Normal file
55
scripts/gowiki_init
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/bin/sh
|
||||||
|
### BEGIN INIT INFO
|
||||||
|
# Provides: gowiki
|
||||||
|
# Required-Start: $syslog $network
|
||||||
|
# Required-Stop: $syslog
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: GoWiki
|
||||||
|
# Description: GoWiki
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||||
|
DESC="wiki engine"
|
||||||
|
NAME=gowiki
|
||||||
|
DAEMON=/usr/bin/${NAME}
|
||||||
|
DAEMON_OPTS="-s"
|
||||||
|
PID=$(ps aux|grep "${DAEMON}"|awk '!/grep/{print $2}')
|
||||||
|
|
||||||
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
|
do_start() {
|
||||||
|
${DAEMON} ${DAEMON_OPTS}
|
||||||
|
log_end_msg ${?}
|
||||||
|
}
|
||||||
|
|
||||||
|
do_stop() {
|
||||||
|
kill ${PID}
|
||||||
|
return "${?}"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1}" in
|
||||||
|
start)
|
||||||
|
log_action_msg "Starting ${DESC}" "${NAME}"
|
||||||
|
do_start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
log_daemon_msg "Stopping ${DESC}" "${NAME}"
|
||||||
|
do_stop
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
log_daemon_msg "Restarting ${DESC}" "${NAME}"
|
||||||
|
do_stop
|
||||||
|
if [ ${?} -ne 0 ]; then
|
||||||
|
log_end_msg 1
|
||||||
|
fi
|
||||||
|
do_start
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_of_proc -p ${PID} "${DAEMON}" "${NAME}" && exit 0 || exit ${?}
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: service ${NAME} {start|stop|restart|status}"
|
||||||
|
exit 3
|
||||||
|
;;
|
||||||
|
esac
|
||||||
4
scripts/install_linux.sh
Executable file
4
scripts/install_linux.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
useradd wiki
|
||||||
|
mkdir /var/{lib,log}/gowiki
|
||||||
|
chown wiki: /var/{lib,log}/gowiki
|
||||||
32
scripts/install_openbsd.sh
Executable file
32
scripts/install_openbsd.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# rc script
|
||||||
|
cat <<'EOF'> /etc/rc.d/gowiki
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
daemon="/usr/local/sbin/gowiki"
|
||||||
|
|
||||||
|
. /etc/rc.d/rc.subr
|
||||||
|
|
||||||
|
rc_cmd $1
|
||||||
|
EOF
|
||||||
|
chmod 555 /etc/rc.d/gowiki
|
||||||
|
rcctl enable gowiki
|
||||||
|
rcctl set gowiki flags server -c /var/wiki/gowiki.conf
|
||||||
|
|
||||||
|
# user
|
||||||
|
useradd -m -b /var -s /sbin/nologin wiki
|
||||||
|
|
||||||
|
# config
|
||||||
|
cat <<EOF> /var/wiki/gowiki.conf
|
||||||
|
{
|
||||||
|
"data_dir": "/var/wiki",
|
||||||
|
"secret": "$(openssl rand -hex 32)",
|
||||||
|
"secure_cookie": true
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
chmod 640 /var/wiki/gowiki.conf
|
||||||
|
chgrp wiki /var/wiki/gowiki.conf
|
||||||
|
|
||||||
|
# start
|
||||||
|
rcctl start gowiki
|
||||||
19
sri.sh
Executable file
19
sri.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
HASH="sha256"
|
||||||
|
DIR="templates"
|
||||||
|
FILES="*.css *.js"
|
||||||
|
BINS="*.ico"
|
||||||
|
echo "SRI (subresource integrity) hashes"
|
||||||
|
cd ${DIR}
|
||||||
|
for f in $(ls ${FILES} 2>/dev/null); do
|
||||||
|
grep '^' ${f} | head -c-1 - > tmp
|
||||||
|
mv tmp ${f}
|
||||||
|
printf "%13s: " ${f}
|
||||||
|
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||||
|
done
|
||||||
|
for f in $(ls ${BINS} 2>/dev/null); do
|
||||||
|
printf "%13s: " ${f}
|
||||||
|
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||||
|
done
|
||||||
112
templates.sh
Executable file
112
templates.sh
Executable file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
# this file generates Go source code for the gowiki
|
||||||
|
# it contains html and css and uses the contents
|
||||||
|
# of the templates directory
|
||||||
|
|
||||||
|
templates_go="pkg/server/templates.go"
|
||||||
|
echo "Generating ${templates_go}"
|
||||||
|
|
||||||
|
if [ ! -d templates ]; then
|
||||||
|
echo "error: templates directory not found" 1>&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat << EOF > ${templates_go}
|
||||||
|
// Copyright (C) 2019 Marius Schellenberger
|
||||||
|
|
||||||
|
// This file is auto-generated by templates.sh
|
||||||
|
// using the contents of the templates directory
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"git.giftfish.de/ston1th/docstore/pkg/log"
|
||||||
|
"html/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "const (" >> ${templates_go}
|
||||||
|
|
||||||
|
for f in $(ls templates/*.html); do
|
||||||
|
fname=${f/templates\//}
|
||||||
|
fname=${fname/.html/}
|
||||||
|
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in $(ls templates/*.css); do
|
||||||
|
fname=${f/templates\//}
|
||||||
|
fname=${fname/.css/CSS}
|
||||||
|
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " favicon = \"$(base64 -w 0 templates/favicon.ico)\"" >> ${templates_go}
|
||||||
|
|
||||||
|
cat << EOF >> ${templates_go}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *HTTPServer) loadTemplates() {
|
||||||
|
var fatal bool
|
||||||
|
s.templ = make(map[string]*template.Template)
|
||||||
|
s.res = make(map[string][]byte)
|
||||||
|
parse := func(html ...string) (temp *template.Template) {
|
||||||
|
var err error
|
||||||
|
temp = template.New("")
|
||||||
|
for _, s := range html {
|
||||||
|
temp, err = temp.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("parse:", err)
|
||||||
|
fatal = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// resources
|
||||||
|
s.res["bootstrap.css"] = []byte(bootstrapCSS)
|
||||||
|
s.res["custom.css"] = []byte(customCSS)
|
||||||
|
fav, err := base64.StdEncoding.DecodeString(favicon)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("favicon:", err)
|
||||||
|
fatal = true
|
||||||
|
}
|
||||||
|
s.res["favicon.ico"] = fav
|
||||||
|
|
||||||
|
// pages
|
||||||
|
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||||
|
s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
|
||||||
|
s.templ["loginHandler"] = parse(index, menu, login)
|
||||||
|
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||||
|
s.templ["searchHandler"] = parse(index, menu, search)
|
||||||
|
s.templ["pageHandler"] = parse(index, menu, page)
|
||||||
|
s.templ["pageNewHandler"] = parse(index, menu, pageNew)
|
||||||
|
s.templ["pageEditHandler"] = parse(index, menu, pageEdit)
|
||||||
|
s.templ["pageViewHandler"] = parse(index, menu, pageView)
|
||||||
|
s.templ["pageMDHandler"] = parse(index, menu, pageMD)
|
||||||
|
s.templ["pageShareHandler"] = parse(index, menu, pageShare)
|
||||||
|
s.templ["pageBlacklistHandler"] = parse(index, menu, pageBlacklist)
|
||||||
|
s.templ["pageDelHandler"] = parse(index, menu, pageDel)
|
||||||
|
// sections
|
||||||
|
s.templ["sectionHandler"] = parse(index, menu, section)
|
||||||
|
s.templ["sectionsHandler"] = parse(index, menu, sections)
|
||||||
|
s.templ["sectionNewHandler"] = parse(index, menu, sectionNew)
|
||||||
|
s.templ["sectionEditHandler"] = parse(index, menu, sectionEdit)
|
||||||
|
s.templ["sectionDelHandler"] = parse(index, menu, sectionDel)
|
||||||
|
// users
|
||||||
|
s.templ["usersHandler"] = parse(index, menu, users)
|
||||||
|
s.templ["userNewHandler"] = parse(index, menu, userNew)
|
||||||
|
s.templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||||
|
s.templ["userDelHandler"] = parse(index, menu, userDel)
|
||||||
|
// totp
|
||||||
|
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||||
|
|
||||||
|
if fatal {
|
||||||
|
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
12
templates/bootstrap.css
vendored
Normal file
12
templates/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
149
templates/custom.css
Normal file
149
templates/custom.css
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.sm-pull-right {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
.sm-pull-left {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.md-pull-right {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
.md-pull-left {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.lg-pull-right {
|
||||||
|
float: right !important;
|
||||||
|
}
|
||||||
|
.lg-pull-left {
|
||||||
|
float: left !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding-top: 60px;
|
||||||
|
}
|
||||||
|
.msg {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
a.dark {
|
||||||
|
color: #222222;
|
||||||
|
}
|
||||||
|
a.para {
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
a.anchor {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -70px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.alert-primary {
|
||||||
|
background-color: #375a7f;
|
||||||
|
border-color: #375a7f;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
h1,h2,h3,h4,h5,h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-top: 60px !important;
|
||||||
|
margin-top: -55px !important;
|
||||||
|
-webkit-background-clip: content-box !important;
|
||||||
|
background-clip: content-box !important;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.md-text {
|
||||||
|
font-family: 'Courier New';
|
||||||
|
font-size: 14;
|
||||||
|
}
|
||||||
|
ul.toc {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 0 10.5px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
color: #0cdba6;
|
||||||
|
background-color: #444444;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
pre.wrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
padding: 3px 3px 1px 0px;
|
||||||
|
font-size: 90%;
|
||||||
|
color: #0cdba6;
|
||||||
|
background-color: #444444;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
textarea,input,select {
|
||||||
|
color: #e2e2e2 !important;
|
||||||
|
background-color: #444444 !important;
|
||||||
|
}
|
||||||
|
select.sections {
|
||||||
|
height: 120px !important;
|
||||||
|
}
|
||||||
|
input.menu {
|
||||||
|
color: #e2e2e2 !important;
|
||||||
|
background-color: #222222 !important;
|
||||||
|
}
|
||||||
|
.menu-search-bar {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
.menu-search-input {
|
||||||
|
background-color: #2f2f2f !important;
|
||||||
|
}
|
||||||
|
html, body, .container, .content {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
.container, .content {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.proper-content {
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -60px;
|
||||||
|
}
|
||||||
|
.push {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
.footer-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 20px;
|
||||||
|
height: 50px;
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
BIN
templates/favicon.ico
Normal file
BIN
templates/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
17
templates/forbidden.html
Normal file
17
templates/forbidden.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>403 - Forbidden</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Access to this page is restricted</h5>
|
||||||
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
46
templates/index.html
Normal file
46
templates/index.html
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>GoWiki | {{.Title}}</title>
|
||||||
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-5/Vbh+c9BC042HeSbJXZZVIc1dD2b9cy6uH9LFAOSfo="></link>
|
||||||
|
<meta name="theme-color" content="#375a7f">
|
||||||
|
<meta name="description" content="{{.Title}}">
|
||||||
|
</head>
|
||||||
|
<body id="top">
|
||||||
|
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="navbar-brand">GoWiki</a>
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
{{template "menu" .}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="proper-content">
|
||||||
|
{{if .Msg}}
|
||||||
|
<div class="alert alert-danger msg">
|
||||||
|
<h4>Error!</h4>
|
||||||
|
<p>{{.Msg}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{template "body" .}}
|
||||||
|
</div>
|
||||||
|
<div class="push"></div>
|
||||||
|
<footer>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li class="float-md-right"><a href="#top">Back to top</a></li>
|
||||||
|
{{if .Login}}
|
||||||
|
<li><a href="/blacklist">Blacklist</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
34
templates/login.html
Normal file
34
templates/login.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/login" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<input type="hidden" name="referer" value="{{.Data.Referer}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="user">Username</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.User}}" autofocus required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="password">Password</label>
|
||||||
|
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="remember" name="remember">
|
||||||
|
<label class="custom-control-label" for="remember">Remember Me</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
24
templates/loginTotp.html
Normal file
24
templates/loginTotp.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/totp" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="pin">PIN</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Verify</button>
|
||||||
|
<a href="/logout" class="btn btn-sm btn-primary">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
26
templates/menu.html
Normal file
26
templates/menu.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{{define "menu"}}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/wiki/Index">Index</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/sections">Sections</a></li>
|
||||||
|
{{if .Login}}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/new">New Page</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<form class="form-inline" method="post" action="/search">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
|
||||||
|
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
<ul class="nav navbar-nav ml-auto">
|
||||||
|
{{if .Login}}
|
||||||
|
{{if .Admin}}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
|
||||||
|
{{end}}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||||
|
{{else}}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
17
templates/notFound.html
Normal file
17
templates/notFound.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>404 - Page Not Found</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>The requested page could not be found</h5>
|
||||||
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
43
templates/page.html
Normal file
43
templates/page.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||||
|
{{if .Data.Updated}}
|
||||||
|
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||||
|
{{end}}
|
||||||
|
<strong>Visibility:</strong>
|
||||||
|
{{if eq .Data.Perm 1}}
|
||||||
|
<span class="badge badge-primary">Public</span>
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Data.Perm 2}}
|
||||||
|
<span class="badge badge-danger">Private</span>
|
||||||
|
{{end}}
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col">
|
||||||
|
{{.Data.TOC}}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group float-md-right">
|
||||||
|
<a href="/{{.Data.StoreTitle}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||||
|
{{if .Login}}
|
||||||
|
<a href="/{{.Data.StoreTitle}}/edit" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
{{if eq .Data.Perm 3}}
|
||||||
|
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Data.StoreTitle "wiki/Index"}}
|
||||||
|
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{.Data.HTML}}
|
||||||
|
{{end}}
|
||||||
23
templates/pageBlacklist.html
Normal file
23
templates/pageBlacklist.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/blacklist" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="token">Token</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="token" name="share" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Blacklist</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
20
templates/pageDel.html
Normal file
20
templates/pageDel.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>Delete {{.Data}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/{{.Data}}/del" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||||
|
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
30
templates/pageEdit.html
Normal file
30
templates/pageEdit.html
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="form-horizontal" action="/{{.Data.StoreTitle}}/edit" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="title">Title</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="title" name="title" value="{{.Data.Title}}" spellcheck="false" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="markdown">Markdown</label>
|
||||||
|
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Markdown}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Perm 1}}checked=""{{end}}>
|
||||||
|
<label class="custom-control-label" for="perm1">Public</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Perm 2}}checked=""{{end}}>
|
||||||
|
<label class="custom-control-label" for="perm2">Private</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
|
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
10
templates/pageMD.html
Normal file
10
templates/pageMD.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{.Data.Markdown}}</textarea>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
41
templates/pageNew.html
Normal file
41
templates/pageNew.html
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form class="form-horizontal" action="/new" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="section">Section</label>
|
||||||
|
<select class="form-control input-sm" id="section" name="section" required>
|
||||||
|
{{range $section, $selected := .Data.Sections}}
|
||||||
|
{{if $selected}}
|
||||||
|
<option value="{{$section}}" selected>{{$section}}</option>
|
||||||
|
{{else}}
|
||||||
|
<option value="{{$section}}">{{$section}}</option>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="title">Title</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="title" name="title" placeholder="Title" value="{{.Data.Page.Title}}" spellcheck="false" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="markdown">Markdown</label>
|
||||||
|
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Page.Markdown}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Page.Perm 1}}checked=""{{end}}>
|
||||||
|
<label class="custom-control-label" for="perm1">Public</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Page.Perm 2}}checked=""{{end}}>
|
||||||
|
<label class="custom-control-label" for="perm2">Private</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
43
templates/pageShare.html
Normal file
43
templates/pageShare.html
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>Share {{.Data}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{if .Key}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="key">Key</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
||||||
|
</div>
|
||||||
|
<a href="/view/{{.Key}}" class="btn btn-sm btn-danger">Share Link</a>
|
||||||
|
<a href="/{{.Data}}/share" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
{{else}}
|
||||||
|
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="duration">Lifetime</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="duration" name="duration" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="mode1" name="mode" value="1" class="custom-control-input" checked="">
|
||||||
|
<label class="custom-control-label" for="mode1">Parse (15m, 10h)</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-radio">
|
||||||
|
<input type="radio" id="mode2" name="mode" value="2" class="custom-control-input">
|
||||||
|
<label class="custom-control-label" for="mode2">Day(s) (Numbers only)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
||||||
|
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
34
templates/pageView.html
Normal file
34
templates/pageView.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col">
|
||||||
|
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||||
|
{{if .Data.Updated}}
|
||||||
|
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||||
|
{{end}}
|
||||||
|
<strong>Visibility:</strong>
|
||||||
|
{{if eq .Data.Perm 1}}
|
||||||
|
<span class="badge badge-primary">Public</span>
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Data.Perm 2}}
|
||||||
|
<span class="badge badge-danger">Private</span>
|
||||||
|
{{end}}
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col">
|
||||||
|
{{.Data.TOC}}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="btn-group float-md-right">
|
||||||
|
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{.Data.HTML}}
|
||||||
|
{{end}}
|
||||||
30
templates/search.html
Normal file
30
templates/search.html
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<form class="form-horizontal" action="/search" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col">
|
||||||
|
{{range $item := .Data}}
|
||||||
|
<a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a>
|
||||||
|
<pre class="wrap">{{$item.HTML}}</pre><br>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
25
templates/section.html
Normal file
25
templates/section.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{{if .Admin}}
|
||||||
|
<div class="btn-group float-right">
|
||||||
|
<a class="btn btn-sm btn-primary" href="/section/edit/{{.BodyTitle}}">Edit</a>
|
||||||
|
<a class="btn btn-sm btn-danger" href="/section/del/{{.BodyTitle}}">Delete</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="row">
|
||||||
|
<ul type="circle">
|
||||||
|
{{range $item := .Data}}
|
||||||
|
<li><a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
23
templates/sectionDel.html
Normal file
23
templates/sectionDel.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{{define "body"}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>Delete {{.Data}}?</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>You are about to delete section <a href="/{{.Data}}">{{.Data}}</a>.<br>This will also remove all pages of this section.</p>
|
||||||
|
<form class="form-horizontal" action="/section/del/{{.Data}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||||
|
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
34
templates/sectionEdit.html
Normal file
34
templates/sectionEdit.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/section/edit/{{.Data.Name}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="section">Section</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="section" name="section" value="{{.Data.Name}}" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="members">Members</label>
|
||||||
|
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||||
|
{{range $user, $member := .Data.Members}}
|
||||||
|
{{if $member}}
|
||||||
|
<option value="{{$user}}" selected>{{$user}}</option>
|
||||||
|
{{else}}
|
||||||
|
<option value="{{$user}}">{{$user}}</option>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
|
<a href="/{{.Data.Name}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
34
templates/sectionNew.html
Normal file
34
templates/sectionNew.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/section/new" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="section">Section</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="section" name="section" autocomplete="off" value="{{.Data.Name}}" autofocus required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="members">Members</label>
|
||||||
|
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||||
|
{{range $user, $member := .Data.Members}}
|
||||||
|
{{if $member}}
|
||||||
|
<option value="{{$user}}" selected>{{$user}}</option>
|
||||||
|
{{else}}
|
||||||
|
<option value="{{$user}}">{{$user}}</option>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
|
<a href="/sections" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
33
templates/sections.html
Normal file
33
templates/sections.html
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Admin}}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-xs-5">
|
||||||
|
<a class="btn btn-sm btn-primary" href="/section/new"><b>New Section</b></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="row">
|
||||||
|
{{range $index, $item := .Data}}
|
||||||
|
<div class="col-xs-4 section">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong><a href="/{{$item.Section}}">{{$item.Section}}</a></strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{{range $page := $item.Pages}}
|
||||||
|
<strong><a href="/{{$page.StoreTitle}}">{{$page.Title}}</a></strong><br>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="offset-1"></div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
23
templates/userDel.html
Normal file
23
templates/userDel.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{{define "body"}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>Delete {{.Data.Username}}?</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>You are about to delete user <a href="/{{.Data.Username}}">{{.Data.Username}}</a>.<br>This will also remove all pages of this user.</p>
|
||||||
|
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||||
|
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
63
templates/userEdit.html
Normal file
63
templates/userEdit.html
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
{{define "body"}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
{{if .Data.Secret}}
|
||||||
|
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="user">Username</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="password">Password</label>
|
||||||
|
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="repeat">Repeat</label>
|
||||||
|
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
|
||||||
|
</div>
|
||||||
|
{{if .Admin}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="admin">Admin Privileges</label>
|
||||||
|
{{if .Data.Admin}}
|
||||||
|
<input type="checkbox" id="admin" name="admin" value="0" checked>
|
||||||
|
{{else}}
|
||||||
|
<input type="checkbox" id="admin" name="admin" value="0">
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
|
{{if .Admin}}
|
||||||
|
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||||
|
</form>
|
||||||
|
{{if .Admin}}
|
||||||
|
{{if eq .Data.Locked 3}}
|
||||||
|
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="unlock">Locked</label>
|
||||||
|
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
34
templates/userNew.html
Normal file
34
templates/userNew.html
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/user/new" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="user">Username</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="password">Password</label>
|
||||||
|
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="repeat">Repeat</label>
|
||||||
|
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="admin">Admin Privileges</label>
|
||||||
|
<input type="checkbox" id="admin" name="admin" value="0">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
|
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
38
templates/userTotp.html
Normal file
38
templates/userTotp.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{{define "body"}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-primary mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>{{.BodyTitle}} - {{.Data.Username}}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
{{if .Data.Secret}}
|
||||||
|
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||||
|
<div class="form-group">
|
||||||
|
<img src="{{.Data.Image}}" alt="{{.Data.Secret}}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="secret">Secret</label>
|
||||||
|
<pre><code id="secret">{{.Data.Secret}}</code></pre>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-form-label" for="pin">PIN</label>
|
||||||
|
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
{{if .Data.Secret}}
|
||||||
|
<button class="btn btn-sm btn-success" type="submit">Enable</button>
|
||||||
|
{{else}}
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||||
|
{{end}}
|
||||||
|
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
55
templates/users.html
Normal file
55
templates/users.html
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .Admin}}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-xs-5">
|
||||||
|
<a class="btn btn-sm btn-primary" href="/user/new"><b>New User</b></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
{{if .Data}}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Flags</th>
|
||||||
|
<th>Options</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{$user := .User}}
|
||||||
|
{{$admin := .Admin}}
|
||||||
|
{{range $item := .Data}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$item.Username}}</td>
|
||||||
|
<td>
|
||||||
|
{{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}
|
||||||
|
{{if eq $item.Locked 3}}<span class="badge badge-warning">Locked</span>{{end}}
|
||||||
|
{{if $item.Admin}}<span class="badge badge-danger">Admin</span>{{end}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
{{if $admin}}
|
||||||
|
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||||
|
<a class="btn btn-sm btn-danger" href="/user/del/{{$item.Username}}">Delete</a>
|
||||||
|
{{else}}
|
||||||
|
{{if eq $item.Username $user}}
|
||||||
|
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
3
vendor/git.giftfish.de/ston1th/godrop/v2/.gitignore
vendored
Normal file
3
vendor/git.giftfish.de/ston1th/godrop/v2/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
port80
|
||||||
|
port80fg
|
||||||
|
port80and443
|
||||||
24
vendor/git.giftfish.de/ston1th/godrop/v2/LICENSE
vendored
Normal file
24
vendor/git.giftfish.de/ston1th/godrop/v2/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (C) 2017 Marius Schellenberger
|
||||||
|
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 authors and/or 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 ston1th 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.
|
||||||
5
vendor/git.giftfish.de/ston1th/godrop/v2/README.md
vendored
Normal file
5
vendor/git.giftfish.de/ston1th/godrop/v2/README.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# godrop - drop privileges
|
||||||
|
|
||||||
|
Godrop is a simple library to drop privileges on Linux and OpenBSD.
|
||||||
|
|
||||||
|
See the examples directory on how to use the `Drop` and `MultiDrop` functions.
|
||||||
3
vendor/git.giftfish.de/ston1th/godrop/v2/go.mod
vendored
Normal file
3
vendor/git.giftfish.de/ston1th/godrop/v2/go.mod
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.giftfish.de/ston1th/godrop/v2
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20181026144532-2772b66316d2 // indirect
|
||||||
2
vendor/git.giftfish.de/ston1th/godrop/v2/go.sum
vendored
Normal file
2
vendor/git.giftfish.de/ston1th/godrop/v2/go.sum
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
golang.org/x/sys v0.0.0-20181026144532-2772b66316d2 h1:W7CqTdBJ1CmxLKe7LptKDnBYV6PHrVLiGnoyBjaG/JQ=
|
||||||
|
golang.org/x/sys v0.0.0-20181026144532-2772b66316d2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
136
vendor/git.giftfish.de/ston1th/godrop/v2/godrop.go
vendored
Normal file
136
vendor/git.giftfish.de/ston1th/godrop/v2/godrop.go
vendored
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build go1.11
|
||||||
|
|
||||||
|
// Package godrop provides a simple library to drop privileges on Linux and OpenBSD.
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the drop config
|
||||||
|
type Config struct {
|
||||||
|
// User is the user to drop privileges to.
|
||||||
|
User string
|
||||||
|
// Group is the group to drop privileges to.
|
||||||
|
Group string
|
||||||
|
// Chroot is the directory to chroot into. Leave this emptry for no chroot.
|
||||||
|
// When compiling without cgo, make sure the chroot directory contains the /etc/passwd and /etc/group files.
|
||||||
|
Chroot string
|
||||||
|
// Set to true, to run the process in the foreground.
|
||||||
|
Foreground bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop will spawn a new process and hand over the listening socket file descriptor
|
||||||
|
func Drop(c Config, f func() (net.Listener, error)) error {
|
||||||
|
return MultiDrop(c, func() ([]net.Listener, error) {
|
||||||
|
l, err := f()
|
||||||
|
return []net.Listener{l}, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiDrop will spawn a new process and hand over the all listening sockets
|
||||||
|
func MultiDrop(c Config, f func() ([]net.Listener, error)) error {
|
||||||
|
uid, err := userID(c.User)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("godrop: " + err.Error())
|
||||||
|
}
|
||||||
|
if uid == 0 {
|
||||||
|
return fmt.Errorf("godrop: you can't drop privileges to uid 0 (%s)", c.User)
|
||||||
|
}
|
||||||
|
gid, err := groupID(c.Group)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("godrop: " + err.Error())
|
||||||
|
}
|
||||||
|
switch os.Getuid() {
|
||||||
|
case 0:
|
||||||
|
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||||
|
ln, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("godrop: " + err.Error())
|
||||||
|
}
|
||||||
|
for i, v := range ln {
|
||||||
|
var f *os.File
|
||||||
|
switch l := v.(type) {
|
||||||
|
case *net.TCPListener:
|
||||||
|
f, err = l.File()
|
||||||
|
l.Close()
|
||||||
|
case *net.UnixListener:
|
||||||
|
f, err = l.File()
|
||||||
|
l.Close()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("godrop: index %d listener is not type of either *net.TCPListener or *net.UnixListener", i)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("godrop: index %d %s", i, err)
|
||||||
|
}
|
||||||
|
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Chroot: c.Chroot,
|
||||||
|
Credential: &syscall.Credential{
|
||||||
|
Uid: uint32(uid),
|
||||||
|
Gid: uint32(gid),
|
||||||
|
},
|
||||||
|
Setsid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Foreground {
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return errors.New("godrop: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Foreground {
|
||||||
|
go func() {
|
||||||
|
term := make(chan os.Signal)
|
||||||
|
signal.Notify(term, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
sigs := make(chan os.Signal)
|
||||||
|
signal.Notify(sigs, syscall.SIGSTOP, syscall.SIGCONT, syscall.SIGUSR1, syscall.SIGUSR2)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case sig := <-term:
|
||||||
|
cmd.Process.Signal(sig)
|
||||||
|
return
|
||||||
|
case sig := <-sigs:
|
||||||
|
cmd.Process.Signal(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_ = cmd.Wait()
|
||||||
|
os.Exit(int(cmd.ProcessState.Sys().(syscall.WaitStatus)))
|
||||||
|
}
|
||||||
|
cmd.Process.Release()
|
||||||
|
os.Exit(0)
|
||||||
|
case uid:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("godrop: dropping priviledges failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListener returns the listener socket of file descriptor 3
|
||||||
|
func GetListener() (net.Listener, error) {
|
||||||
|
return GetListenerFd(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListenerFd returns the listener socket of the given file descriptor
|
||||||
|
func GetListenerFd(fd int) (net.Listener, error) {
|
||||||
|
if fd < 3 {
|
||||||
|
return nil, errors.New("godrop: fd is less than 3")
|
||||||
|
}
|
||||||
|
f := os.NewFile(uintptr(fd), "")
|
||||||
|
defer f.Close()
|
||||||
|
return net.FileListener(f)
|
||||||
|
}
|
||||||
34
vendor/git.giftfish.de/ston1th/godrop/v2/lookup.go
vendored
Normal file
34
vendor/git.giftfish.de/ston1th/godrop/v2/lookup.go
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build go1.11
|
||||||
|
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/user"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func atoi(a string) (int, error) {
|
||||||
|
i, err := strconv.Atoi(a)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func userID(username string) (int, error) {
|
||||||
|
u, err := user.Lookup(username)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return atoi(u.Uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupID(name string) (int, error) {
|
||||||
|
g, err := user.LookupGroup(name)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
return atoi(g.Gid)
|
||||||
|
}
|
||||||
14
vendor/git.giftfish.de/ston1th/godrop/v2/pledge.go
vendored
Normal file
14
vendor/git.giftfish.de/ston1th/godrop/v2/pledge.go
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build !openbsd
|
||||||
|
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
// Pledge is currently only supported on OpenBSD.
|
||||||
|
func Pledge(promises, execpromises string) error { return nil }
|
||||||
|
|
||||||
|
// PledgePromises is currently only supported on OpenBSD.
|
||||||
|
func PledgePromises(promises string) error { return nil }
|
||||||
|
|
||||||
|
// PledgeExecPromises is currently only supported on OpenBSD.
|
||||||
|
func PledgeExecpromises(execpromises string) error { return nil }
|
||||||
43
vendor/git.giftfish.de/ston1th/godrop/v2/pledge_openbsd.go
vendored
Normal file
43
vendor/git.giftfish.de/ston1th/godrop/v2/pledge_openbsd.go
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build openbsd
|
||||||
|
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pledge is a wrapper for x/sys/unix Pledge.
|
||||||
|
//
|
||||||
|
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
|
||||||
|
func Pledge(promises, execpromises string) (err error) {
|
||||||
|
err = unix.Pledge(promises, execpromises)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("pledge: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PledgePromises is a wrapper for x/sys/unix PledgePromises.
|
||||||
|
//
|
||||||
|
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
|
||||||
|
func PledgePromises(promises string) (err error) {
|
||||||
|
err = unix.PledgePromises(promises)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("pledge: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PledgeExecpromises is a wrapper for x/sys/unix PledgeExecpromises.
|
||||||
|
//
|
||||||
|
// See https://go.googlesource.com/sys/+/master/unix/openbsd_pledge.go for usage.
|
||||||
|
func PledgeExecpromises(execpromises string) (err error) {
|
||||||
|
err = unix.PledgeExecpromises(execpromises)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("pledge: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
11
vendor/git.giftfish.de/ston1th/godrop/v2/unveil.go
vendored
Normal file
11
vendor/git.giftfish.de/ston1th/godrop/v2/unveil.go
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build !openbsd
|
||||||
|
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
// Unveil is currently only supported on OpenBSD.
|
||||||
|
func Unveil(path, flags string) error { return nil }
|
||||||
|
|
||||||
|
// UnveilBlock is currently only supported on OpenBSD.
|
||||||
|
func UnveilBlock() error { return nil }
|
||||||
32
vendor/git.giftfish.de/ston1th/godrop/v2/unveil_openbsd.go
vendored
Normal file
32
vendor/git.giftfish.de/ston1th/godrop/v2/unveil_openbsd.go
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// +build openbsd
|
||||||
|
|
||||||
|
package godrop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unveil is a wrapper for x/sys/unix Unveil.
|
||||||
|
//
|
||||||
|
// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage.
|
||||||
|
func Unveil(path, flags string) (err error) {
|
||||||
|
err = unix.Unveil(path, flags)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("unveil: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnveilBlock is a wrapper for x/sys/unix UnveilBlock.
|
||||||
|
//
|
||||||
|
// See https://go.googlesource.com/sys/+/master/unix/openbsd_unveil.go for usage.
|
||||||
|
func UnveilBlock() (err error) {
|
||||||
|
err = unix.UnveilBlock()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("unveil: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
24
vendor/git.giftfish.de/ston1th/jwt/v3/LICENSE
vendored
Normal file
24
vendor/git.giftfish.de/ston1th/jwt/v3/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (C) 2018 Marius Schellenberger
|
||||||
|
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 authors and/or 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 ston1th 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.
|
||||||
61
vendor/git.giftfish.de/ston1th/jwt/v3/README.md
vendored
Normal file
61
vendor/git.giftfish.de/ston1th/jwt/v3/README.md
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# jwt - a simple JSON Web Token library written in Go
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.giftfish.de/ston1th/jwt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// create a new signing instance
|
||||||
|
JWT, err := jwt.New(time.Hour, jwt.NewMemBlacklist(), nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new token
|
||||||
|
t := jwt.NewToken(jwt.Claims{
|
||||||
|
"username": "admin",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
// sign the token
|
||||||
|
err = JWT.Sign(t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print the token string
|
||||||
|
token := t.String()
|
||||||
|
fmt.Println(token)
|
||||||
|
|
||||||
|
// decode the token string
|
||||||
|
newToken, err := jwt.DecodeToken(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the decoded token
|
||||||
|
err = JWT.Verify(newToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the token to the blacklist
|
||||||
|
err = JWT.Invalidate(newToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to verify the blacklisted token, it should fail
|
||||||
|
err = JWT.Verify(newToken)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
63
vendor/git.giftfish.de/ston1th/jwt/v3/blacklist.go
vendored
Normal file
63
vendor/git.giftfish.de/ston1th/jwt/v3/blacklist.go
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// Blacklist is the blacklisting storage interface
|
||||||
|
type Blacklist interface {
|
||||||
|
Add(string, int64) error
|
||||||
|
Remove(string) error
|
||||||
|
Check(string) bool
|
||||||
|
Map() (BlacklistMap, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlacklistMap is the blacklist map structure
|
||||||
|
type BlacklistMap map[string]int64
|
||||||
|
|
||||||
|
// MemBlacklist implements the Blacklist interface
|
||||||
|
type MemBlacklist struct {
|
||||||
|
// protects list
|
||||||
|
sync.RWMutex
|
||||||
|
list BlacklistMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemBlacklist implements the Blacklist interface using an in-memory map
|
||||||
|
func NewMemBlacklist() *MemBlacklist {
|
||||||
|
return &MemBlacklist{list: make(BlacklistMap)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a new token signature with expiration time to the blacklist
|
||||||
|
func (mb *MemBlacklist) Add(sig string, exp int64) error {
|
||||||
|
mb.Lock()
|
||||||
|
mb.list[sig] = exp
|
||||||
|
mb.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deletes a token signature from the blacklist
|
||||||
|
func (mb *MemBlacklist) Remove(sig string) error {
|
||||||
|
mb.Lock()
|
||||||
|
delete(mb.list, sig)
|
||||||
|
mb.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check returns true if a token signature is blacklisted and false otherwise
|
||||||
|
func (mb *MemBlacklist) Check(sig string) (ok bool) {
|
||||||
|
mb.RLock()
|
||||||
|
_, ok = mb.list[sig]
|
||||||
|
mb.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map returns the blacklist in the form of a iterable map structure for cleanup
|
||||||
|
func (mb *MemBlacklist) Map() (list BlacklistMap, err error) {
|
||||||
|
list = make(BlacklistMap)
|
||||||
|
mb.RLock()
|
||||||
|
for k, v := range mb.list {
|
||||||
|
list[k] = v
|
||||||
|
}
|
||||||
|
mb.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
87
vendor/git.giftfish.de/ston1th/jwt/v3/claims.go
vendored
Normal file
87
vendor/git.giftfish.de/ston1th/jwt/v3/claims.go
vendored
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
// Claims is the claim type of the token
|
||||||
|
// The Claims map is not goroutine safe
|
||||||
|
type Claims map[string]interface{}
|
||||||
|
|
||||||
|
// Get returns a value from the claims map
|
||||||
|
func (c Claims) Get(key string) (v interface{}, ok bool) {
|
||||||
|
v, ok = c[key]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns a string from the claims map
|
||||||
|
func (c Claims) GetString(key string) (s string) {
|
||||||
|
if v, ok := c.Get(key); ok && v != nil {
|
||||||
|
s, _ = v.(string)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool returns a bool from the claims map
|
||||||
|
func (c Claims) GetBool(key string) (b bool) {
|
||||||
|
if v, ok := c.Get(key); ok && v != nil {
|
||||||
|
b, _ = v.(bool)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt returns an int from the claims map
|
||||||
|
func (c Claims) GetInt(key string) (i int) {
|
||||||
|
if f, ok := c.getFloat64(key); ok {
|
||||||
|
return int(f)
|
||||||
|
}
|
||||||
|
if v, ok := c.Get(key); ok && v != nil {
|
||||||
|
i, _ = v.(int)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt64 returns an int64 from the claims map
|
||||||
|
func (c Claims) GetInt64(key string) (i int64) {
|
||||||
|
if f, ok := c.getFloat64(key); ok {
|
||||||
|
return int64(f)
|
||||||
|
}
|
||||||
|
if v, ok := c.Get(key); ok && v != nil {
|
||||||
|
i, _ = v.(int64)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFloat64 returns a float64 and ok from the claims map
|
||||||
|
func (c Claims) getFloat64(key string) (f float64, fok bool) {
|
||||||
|
if v, ok := c.Get(key); ok && v != nil {
|
||||||
|
f, fok = v.(float64)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat64 returns a float64 from the claims map
|
||||||
|
func (c Claims) GetFloat64(key string) (f float64) {
|
||||||
|
f, _ = c.getFloat64(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the value of key in the claims map, if not nil
|
||||||
|
func (c Claims) Set(key string, v interface{}) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the key from the claims map
|
||||||
|
func (c Claims) Delete(key string) {
|
||||||
|
delete(c, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a new Claims map
|
||||||
|
func (c Claims) Copy() (n Claims) {
|
||||||
|
n = make(Claims)
|
||||||
|
for k, v := range c {
|
||||||
|
n[k] = v
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
7
vendor/git.giftfish.de/ston1th/jwt/v3/encoding.go
vendored
Normal file
7
vendor/git.giftfish.de/ston1th/jwt/v3/encoding.go
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
var enc = base64.RawURLEncoding
|
||||||
1
vendor/git.giftfish.de/ston1th/jwt/v3/go.mod
vendored
Normal file
1
vendor/git.giftfish.de/ston1th/jwt/v3/go.mod
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module git.giftfish.de/ston1th/jwt/v3
|
||||||
88
vendor/git.giftfish.de/ston1th/jwt/v3/hash.go
vendored
Normal file
88
vendor/git.giftfish.de/ston1th/jwt/v3/hash.go
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HS256Name = "HS256"
|
||||||
|
HS384Name = "HS384"
|
||||||
|
HS512Name = "HS512"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseHash returns the Hash type equal to the input string
|
||||||
|
func ParseHash(alg string) Hash {
|
||||||
|
switch alg {
|
||||||
|
case HS256Name:
|
||||||
|
return NewHS256()
|
||||||
|
case HS384Name:
|
||||||
|
return NewHS384()
|
||||||
|
case HS512Name:
|
||||||
|
return NewHS512()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash is the hashsum interface for signing the jwt
|
||||||
|
type Hash interface {
|
||||||
|
Hash() hash.Hash
|
||||||
|
Alg() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HS256 implements the Hash interface with SHA256
|
||||||
|
type HS256 struct{}
|
||||||
|
|
||||||
|
// NewHS256 returns a new HS256 instance
|
||||||
|
func NewHS256() Hash {
|
||||||
|
return HS256{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alg returns the algorithm name "HS256"
|
||||||
|
func (HS256) Alg() string {
|
||||||
|
return HS256Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns a new hash.Hash instance of the SHA256 algorithm
|
||||||
|
func (HS256) Hash() hash.Hash {
|
||||||
|
return sha256.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HS384 implements the Hash interface with SHA384
|
||||||
|
type HS384 struct{}
|
||||||
|
|
||||||
|
// NewHS384 returns a new HS384 instance
|
||||||
|
func NewHS384() Hash {
|
||||||
|
return HS384{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alg returns the algorithm name "HS384"
|
||||||
|
func (HS384) Alg() string {
|
||||||
|
return HS384Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns a new hash.Hash instance of the SHA384 algorithm
|
||||||
|
func (HS384) Hash() hash.Hash {
|
||||||
|
return sha512.New384()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HS512 implements the Hash interface with SHA512
|
||||||
|
type HS512 struct{}
|
||||||
|
|
||||||
|
// NewHS512 returns a new HS512 instance
|
||||||
|
func NewHS512() Hash {
|
||||||
|
return HS512{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alg returns the algorithm name "HS512"
|
||||||
|
func (HS512) Alg() string {
|
||||||
|
return HS512Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash returns a new hash.Hash instance of the SHA512 algorithm
|
||||||
|
func (HS512) Hash() hash.Hash {
|
||||||
|
return sha512.New()
|
||||||
|
}
|
||||||
266
vendor/git.giftfish.de/ston1th/jwt/v3/jwt.go
vendored
Normal file
266
vendor/git.giftfish.de/ston1th/jwt/v3/jwt.go
vendored
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
// Package jwt provides a easy to use JSON Web Token and blacklisting library
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HTTPHeader is the default HTTP Authorization header name
|
||||||
|
HTTPHeader = "Authorization"
|
||||||
|
// DefaultExpiry is the default token expiration time
|
||||||
|
DefaultExpiry = time.Hour * 12
|
||||||
|
// KeySize is the secret key size
|
||||||
|
KeySize = 64
|
||||||
|
|
||||||
|
// Typ is the JWT type
|
||||||
|
Typ = "JWT"
|
||||||
|
// TypClaim is the typ claim name
|
||||||
|
TypClaim = "typ"
|
||||||
|
// AlgClaim is the alg claim name
|
||||||
|
AlgClaim = "alg"
|
||||||
|
// ExpClaim is the exp claim name
|
||||||
|
ExpClaim = "exp"
|
||||||
|
// NbfClaim is the nbf claim name
|
||||||
|
NbfClaim = "nbf"
|
||||||
|
// TokenSeparator is the tokens separator char
|
||||||
|
TokenSeparator = "."
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultSecretReader is the default secret key generator
|
||||||
|
var DefaultSecretReader = rand.Reader
|
||||||
|
|
||||||
|
const jwtErr = "jwt: "
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoJWT = errors.New(jwtErr + "not a json web token")
|
||||||
|
ErrEmptyToken = errors.New(jwtErr + "token is empty")
|
||||||
|
ErrUnsupportedAlg = errors.New(jwtErr + "unsupported algorithm")
|
||||||
|
ErrInvalid = errors.New(jwtErr + "token validation failed")
|
||||||
|
ErrBlacklisted = errors.New(jwtErr + "token blacklisted")
|
||||||
|
ErrBlacklistNotEnabled = errors.New(jwtErr + "blacklisting is not enabled")
|
||||||
|
ErrNbf = errors.New(jwtErr + "token not valid yet")
|
||||||
|
ErrExp = errors.New(jwtErr + "token expired")
|
||||||
|
ErrMissingNbf = errors.New(jwtErr + "missing nbf claim")
|
||||||
|
ErrMissingExp = errors.New(jwtErr + "missing exp claim")
|
||||||
|
ErrMissingTokenParts = errors.New(jwtErr + "missing token parts")
|
||||||
|
ErrEmptySignature = errors.New(jwtErr + "token signature is empty")
|
||||||
|
ErrTokenIsNil = errors.New(jwtErr + "token is nil")
|
||||||
|
ErrInvalidKeySize = errors.New(jwtErr + "invalid secret key size")
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWT represents the JSON Web Token signing and blacklisting infrastructure
|
||||||
|
type JWT struct {
|
||||||
|
key []byte
|
||||||
|
expiry time.Duration
|
||||||
|
|
||||||
|
blacklist Blacklist
|
||||||
|
stopOnce sync.Once
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new JWT object with the given expiry timeout.
|
||||||
|
// If the timeout is less or equal to zero the default expiry (12 hours) is used.
|
||||||
|
// The secret key size needs to be at least 64 bytes.
|
||||||
|
// If secret is nil the DefaultSecretReader is used.
|
||||||
|
// If blacklisting is enabled, the JWT object leaks a goroutine to garbage-collect expired blacklisted tokens.
|
||||||
|
// Call the Stop() method to exit the goroutine.
|
||||||
|
func New(expiry time.Duration, blacklist Blacklist, secret io.Reader) (*JWT, error) {
|
||||||
|
if expiry <= 0 {
|
||||||
|
expiry = DefaultExpiry
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
secret = DefaultSecretReader
|
||||||
|
}
|
||||||
|
secret = io.LimitReader(secret, KeySize)
|
||||||
|
key := make([]byte, KeySize)
|
||||||
|
i, err := secret.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(jwtErr + "secret reader error: " + err.Error())
|
||||||
|
}
|
||||||
|
if i < KeySize {
|
||||||
|
return nil, ErrInvalidKeySize
|
||||||
|
}
|
||||||
|
jwt := &JWT{
|
||||||
|
key: key,
|
||||||
|
expiry: expiry,
|
||||||
|
blacklist: blacklist,
|
||||||
|
}
|
||||||
|
if blacklist != nil {
|
||||||
|
jwt.done = make(chan struct{})
|
||||||
|
go jwt.clean()
|
||||||
|
}
|
||||||
|
return jwt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiry returns the configured expiry
|
||||||
|
func (jwt *JWT) Expiry() time.Duration {
|
||||||
|
return jwt.expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
// sum calculates the HMAC hash sum of the token
|
||||||
|
func (jwt *JWT) sum(token string, h Hash) []byte {
|
||||||
|
mac := hmac.New(h.Hash, jwt.key)
|
||||||
|
mac.Write([]byte(token))
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate checks if a token is already blacklisted
|
||||||
|
// If the token is not blacklisted, it will get blacklisted
|
||||||
|
func (jwt *JWT) Invalidate(t *Token) error {
|
||||||
|
if jwt.blacklist == nil {
|
||||||
|
return ErrBlacklistNotEnabled
|
||||||
|
}
|
||||||
|
if t == nil {
|
||||||
|
return ErrTokenIsNil
|
||||||
|
}
|
||||||
|
if err := jwt.blacklisted(t.Sig()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Header.GetString(TypClaim) != Typ {
|
||||||
|
return ErrNoJWT
|
||||||
|
}
|
||||||
|
h := ParseHash(t.Header.GetString(AlgClaim))
|
||||||
|
if h == nil {
|
||||||
|
return ErrUnsupportedAlg
|
||||||
|
}
|
||||||
|
exp := t.Claims.GetInt64(ExpClaim)
|
||||||
|
switch {
|
||||||
|
case exp == 0:
|
||||||
|
return ErrMissingExp
|
||||||
|
case Now() > exp:
|
||||||
|
return ErrExp
|
||||||
|
}
|
||||||
|
if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
return jwt.blacklist.Add(t.Sig(), exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// blacklisted checks if a token is blacklisted
|
||||||
|
func (jwt *JWT) blacklisted(sig string) error {
|
||||||
|
if sig == "" {
|
||||||
|
return ErrEmptySignature
|
||||||
|
}
|
||||||
|
if jwt.blacklist.Check(sig) {
|
||||||
|
return ErrBlacklisted
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean looks for expired blacklisted tokens and removes them
|
||||||
|
func (jwt *JWT) clean() {
|
||||||
|
for {
|
||||||
|
t := time.NewTimer(time.Hour)
|
||||||
|
select {
|
||||||
|
case <-t.C:
|
||||||
|
case <-jwt.done:
|
||||||
|
t.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := Now()
|
||||||
|
m, err := jwt.blacklist.Map()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
if now > v {
|
||||||
|
jwt.blacklist.Remove(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign will sign the provided token using the secret key
|
||||||
|
// If the 'exp' and 'nbf' claims do not exist, they will be written to the default values in UNIX format:
|
||||||
|
// 'exp': Now + expiry specified at New() or DefaultExpiry (UTC)
|
||||||
|
// 'nbf': Now (UTC)
|
||||||
|
func (jwt *JWT) Sign(t *Token) (err error) {
|
||||||
|
if t == nil {
|
||||||
|
return ErrTokenIsNil
|
||||||
|
}
|
||||||
|
if t.Header.GetString(TypClaim) != Typ {
|
||||||
|
return ErrNoJWT
|
||||||
|
}
|
||||||
|
h := ParseHash(t.Header.GetString(AlgClaim))
|
||||||
|
if h == nil {
|
||||||
|
return ErrUnsupportedAlg
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if _, ok := t.Claims.Get(ExpClaim); !ok {
|
||||||
|
t.Claims.Set(ExpClaim, newExp(now, jwt.expiry))
|
||||||
|
}
|
||||||
|
if _, ok := t.Claims.Get(NbfClaim); !ok {
|
||||||
|
t.Claims.Set(NbfClaim, NewNbf(now))
|
||||||
|
}
|
||||||
|
head, err := json.Marshal(t.Header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := json.Marshal(t.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.data = enc.EncodeToString(head) + TokenSeparator + enc.EncodeToString(claims)
|
||||||
|
t.rawSignature = jwt.sum(t.data, h)
|
||||||
|
t.signature = enc.EncodeToString(t.rawSignature)
|
||||||
|
t.raw = t.data + TokenSeparator + t.signature
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify will verify the provided token using the secret key
|
||||||
|
func (jwt *JWT) Verify(t *Token) error {
|
||||||
|
if t == nil {
|
||||||
|
return ErrTokenIsNil
|
||||||
|
}
|
||||||
|
if jwt.blacklist != nil {
|
||||||
|
if err := jwt.blacklisted(t.Sig()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now := Now()
|
||||||
|
if t.Header.GetString(TypClaim) != Typ {
|
||||||
|
return ErrNoJWT
|
||||||
|
}
|
||||||
|
h := ParseHash(t.Header.GetString(AlgClaim))
|
||||||
|
if h == nil {
|
||||||
|
return ErrUnsupportedAlg
|
||||||
|
}
|
||||||
|
exp := t.Claims.GetInt64(ExpClaim)
|
||||||
|
switch {
|
||||||
|
case exp == 0:
|
||||||
|
return ErrMissingExp
|
||||||
|
case now > exp:
|
||||||
|
return ErrExp
|
||||||
|
}
|
||||||
|
nbf := t.Claims.GetInt64(NbfClaim)
|
||||||
|
switch {
|
||||||
|
case nbf == 0:
|
||||||
|
return ErrMissingNbf
|
||||||
|
case now < nbf:
|
||||||
|
return ErrNbf
|
||||||
|
}
|
||||||
|
if !hmac.Equal(jwt.sum(t.Data(), h), t.RawSig()) {
|
||||||
|
return ErrInvalid
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop will end the cleaner goroutine execution
|
||||||
|
func (jwt *JWT) Stop() error {
|
||||||
|
if jwt.blacklist == nil {
|
||||||
|
return ErrBlacklistNotEnabled
|
||||||
|
}
|
||||||
|
jwt.stopOnce.Do(func() {
|
||||||
|
close(jwt.done)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
22
vendor/git.giftfish.de/ston1th/jwt/v3/time.go
vendored
Normal file
22
vendor/git.giftfish.de/ston1th/jwt/v3/time.go
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Now returns the current time in UTC Unix format
|
||||||
|
func Now() int64 {
|
||||||
|
return NewNbf(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNbf returns a new 'not before' date
|
||||||
|
func NewNbf(t time.Time) int64 {
|
||||||
|
return t.UTC().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExp returns a new expiration date
|
||||||
|
func NewExp(d time.Duration) int64 {
|
||||||
|
return newExp(time.Now(), d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExp(t time.Time, d time.Duration) int64 {
|
||||||
|
return t.UTC().Add(d).Unix()
|
||||||
|
}
|
||||||
98
vendor/git.giftfish.de/ston1th/jwt/v3/token.go
vendored
Normal file
98
vendor/git.giftfish.de/ston1th/jwt/v3/token.go
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token represents a JWT token
|
||||||
|
type Token struct {
|
||||||
|
raw string
|
||||||
|
data string
|
||||||
|
signature string
|
||||||
|
rawSignature []byte
|
||||||
|
Header Claims
|
||||||
|
Claims Claims
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the tokens encoded string
|
||||||
|
func (t *Token) String() string {
|
||||||
|
return t.raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sig returns the tokens URLEncoded signature
|
||||||
|
func (t *Token) Sig() string {
|
||||||
|
return t.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawSig returns the tokens raw signature
|
||||||
|
func (t *Token) RawSig() []byte {
|
||||||
|
return t.rawSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data returns the first two URLEncoded token fields
|
||||||
|
func (t *Token) Data() string {
|
||||||
|
return t.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewToken returns a new *Token using the provided hash algorithm and claims
|
||||||
|
// If claims is nil, an empty map is used
|
||||||
|
// If hash is nil, HS256 is used
|
||||||
|
func NewToken(claims Claims, hash Hash) *Token {
|
||||||
|
if claims == nil {
|
||||||
|
claims = make(Claims)
|
||||||
|
}
|
||||||
|
if hash == nil {
|
||||||
|
hash = NewHS256()
|
||||||
|
}
|
||||||
|
return &Token{
|
||||||
|
Header: Claims{
|
||||||
|
AlgClaim: hash.Alg(),
|
||||||
|
TypClaim: Typ,
|
||||||
|
},
|
||||||
|
Claims: claims,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeToken decodes a raw string token into a *Token object
|
||||||
|
func DecodeToken(token string) (t *Token, err error) {
|
||||||
|
if token == "" {
|
||||||
|
return nil, ErrEmptyToken
|
||||||
|
}
|
||||||
|
parts := strings.Split(token, TokenSeparator)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return nil, ErrMissingTokenParts
|
||||||
|
}
|
||||||
|
if len(parts) > 3 {
|
||||||
|
return nil, ErrNoJWT
|
||||||
|
}
|
||||||
|
for _, v := range parts {
|
||||||
|
if v == "" {
|
||||||
|
return nil, ErrNoJWT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = new(Token)
|
||||||
|
header, err := enc.DecodeString(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(header, &t.Header)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
claims, err := enc.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(claims, &t.Claims)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.rawSignature, err = enc.DecodeString(parts[2])
|
||||||
|
t.signature = parts[2]
|
||||||
|
t.data = parts[0] + TokenSeparator + parts[1]
|
||||||
|
t.raw = token
|
||||||
|
return
|
||||||
|
}
|
||||||
6
vendor/github.com/RoaringBitmap/roaring/.gitignore
generated
vendored
Normal file
6
vendor/github.com/RoaringBitmap/roaring/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
*~
|
||||||
|
roaring-fuzz.zip
|
||||||
|
workdir
|
||||||
|
coverage.out
|
||||||
|
testdata/all3.classic
|
||||||
|
testdata/all3.msgp.snappy
|
||||||
0
vendor/github.com/RoaringBitmap/roaring/.gitmodules
generated
vendored
Normal file
0
vendor/github.com/RoaringBitmap/roaring/.gitmodules
generated
vendored
Normal file
30
vendor/github.com/RoaringBitmap/roaring/.travis.yml
generated
vendored
Normal file
30
vendor/github.com/RoaringBitmap/roaring/.travis.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
install:
|
||||||
|
- go get -t github.com/RoaringBitmap/roaring
|
||||||
|
- go get -t golang.org/x/tools/cmd/cover
|
||||||
|
- go get -t github.com/mattn/goveralls
|
||||||
|
- go get -t github.com/mschoch/smat
|
||||||
|
notifications:
|
||||||
|
email: false
|
||||||
|
go:
|
||||||
|
- "1.7.x"
|
||||||
|
- "1.8.x"
|
||||||
|
- "1.9.x"
|
||||||
|
- "1.10.x"
|
||||||
|
- tip
|
||||||
|
|
||||||
|
# whitelist
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- goveralls -v -service travis-ci -ignore arraycontainer_gen.go,bitmapcontainer_gen.go,rle16_gen.go,rle_gen.go,roaringarray_gen.go,rle.go || go test
|
||||||
|
- go test -race -run TestConcurrent*
|
||||||
|
- GOARCH=arm64 go build
|
||||||
|
- GOARCH=386 go build
|
||||||
|
- GOARCH=386 go test
|
||||||
|
- GOARCH=arm go build
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- go: tip
|
||||||
11
vendor/github.com/RoaringBitmap/roaring/AUTHORS
generated
vendored
Normal file
11
vendor/github.com/RoaringBitmap/roaring/AUTHORS
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# This is the official list of roaring authors for copyright purposes.
|
||||||
|
|
||||||
|
Todd Gruben (@tgruben),
|
||||||
|
Daniel Lemire (@lemire),
|
||||||
|
Elliot Murphy (@statik),
|
||||||
|
Bob Potter (@bpot),
|
||||||
|
Tyson Maly (@tvmaly),
|
||||||
|
Will Glynn (@willglynn),
|
||||||
|
Brent Pedersen (@brentp)
|
||||||
|
Maciej Biłas (@maciej),
|
||||||
|
Joe Nall (@joenall)
|
||||||
14
vendor/github.com/RoaringBitmap/roaring/CONTRIBUTORS
generated
vendored
Normal file
14
vendor/github.com/RoaringBitmap/roaring/CONTRIBUTORS
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# This is the official list of roaring contributors
|
||||||
|
|
||||||
|
Todd Gruben (@tgruben),
|
||||||
|
Daniel Lemire (@lemire),
|
||||||
|
Elliot Murphy (@statik),
|
||||||
|
Bob Potter (@bpot),
|
||||||
|
Tyson Maly (@tvmaly),
|
||||||
|
Will Glynn (@willglynn),
|
||||||
|
Brent Pedersen (@brentp),
|
||||||
|
Jason E. Aten (@glycerine),
|
||||||
|
Vali Malinoiu (@0x4139),
|
||||||
|
Forud Ghafouri (@fzerorubigd),
|
||||||
|
Joe Nall (@joenall),
|
||||||
|
(@fredim)
|
||||||
202
vendor/github.com/RoaringBitmap/roaring/LICENSE
generated
vendored
Normal file
202
vendor/github.com/RoaringBitmap/roaring/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2016 by the authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
202
vendor/github.com/RoaringBitmap/roaring/LICENSE-2.0.txt
generated
vendored
Normal file
202
vendor/github.com/RoaringBitmap/roaring/LICENSE-2.0.txt
generated
vendored
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2016 by the authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
111
vendor/github.com/RoaringBitmap/roaring/Makefile
generated
vendored
Normal file
111
vendor/github.com/RoaringBitmap/roaring/Makefile
generated
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
.PHONY: help all test format fmtcheck vet lint qa deps clean nuke ser fetch-real-roaring-datasets
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Display general help about this command
|
||||||
|
help:
|
||||||
|
@echo ""
|
||||||
|
@echo "The following commands are available:"
|
||||||
|
@echo ""
|
||||||
|
@echo " make qa : Run all the tests"
|
||||||
|
@echo " make test : Run the unit tests"
|
||||||
|
@echo ""
|
||||||
|
@echo " make format : Format the source code"
|
||||||
|
@echo " make fmtcheck : Check if the source code has been formatted"
|
||||||
|
@echo " make vet : Check for suspicious constructs"
|
||||||
|
@echo " make lint : Check for style errors"
|
||||||
|
@echo ""
|
||||||
|
@echo " make deps : Get the dependencies"
|
||||||
|
@echo " make clean : Remove any build artifact"
|
||||||
|
@echo " make nuke : Deletes any intermediate file"
|
||||||
|
@echo ""
|
||||||
|
@echo " make fuzz-smat : Fuzzy testing with smat"
|
||||||
|
@echo " make fuzz-stream : Fuzzy testing with stream deserialization"
|
||||||
|
@echo " make fuzz-buffer : Fuzzy testing with buffer deserialization"
|
||||||
|
@echo ""
|
||||||
|
|
||||||
|
# Alias for help target
|
||||||
|
all: help
|
||||||
|
test:
|
||||||
|
go test
|
||||||
|
go test -race -run TestConcurrent*
|
||||||
|
# Format the source code
|
||||||
|
format:
|
||||||
|
@find ./ -type f -name "*.go" -exec gofmt -w {} \;
|
||||||
|
|
||||||
|
# Check if the source code has been formatted
|
||||||
|
fmtcheck:
|
||||||
|
@mkdir -p target
|
||||||
|
@find ./ -type f -name "*.go" -exec gofmt -d {} \; | tee target/format.diff
|
||||||
|
@test ! -s target/format.diff || { echo "ERROR: the source code has not been formatted - please use 'make format' or 'gofmt'"; exit 1; }
|
||||||
|
|
||||||
|
# Check for syntax errors
|
||||||
|
vet:
|
||||||
|
GOPATH=$(GOPATH) go vet ./...
|
||||||
|
|
||||||
|
# Check for style errors
|
||||||
|
lint:
|
||||||
|
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) golint ./...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Alias to run all quality-assurance checks
|
||||||
|
qa: fmtcheck test vet lint
|
||||||
|
|
||||||
|
# --- INSTALL ---
|
||||||
|
|
||||||
|
# Get the dependencies
|
||||||
|
deps:
|
||||||
|
GOPATH=$(GOPATH) go get github.com/smartystreets/goconvey/convey
|
||||||
|
GOPATH=$(GOPATH) go get github.com/willf/bitset
|
||||||
|
GOPATH=$(GOPATH) go get github.com/golang/lint/golint
|
||||||
|
GOPATH=$(GOPATH) go get github.com/mschoch/smat
|
||||||
|
GOPATH=$(GOPATH) go get github.com/dvyukov/go-fuzz/go-fuzz
|
||||||
|
GOPATH=$(GOPATH) go get github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||||
|
GOPATH=$(GOPATH) go get github.com/glycerine/go-unsnap-stream
|
||||||
|
GOPATH=$(GOPATH) go get github.com/philhofer/fwd
|
||||||
|
GOPATH=$(GOPATH) go get github.com/jtolds/gls
|
||||||
|
|
||||||
|
fuzz-smat:
|
||||||
|
go test -tags=gofuzz -run=TestGenerateSmatCorpus
|
||||||
|
go-fuzz-build -func FuzzSmat github.com/RoaringBitmap/roaring
|
||||||
|
go-fuzz -bin=./roaring-fuzz.zip -workdir=workdir/ -timeout=200
|
||||||
|
|
||||||
|
|
||||||
|
fuzz-stream:
|
||||||
|
go-fuzz-build -func FuzzSerializationStream github.com/RoaringBitmap/roaring
|
||||||
|
go-fuzz -bin=./roaring-fuzz.zip -workdir=workdir/ -timeout=200
|
||||||
|
|
||||||
|
|
||||||
|
fuzz-buffer:
|
||||||
|
go-fuzz-build -func FuzzSerializationBuffer github.com/RoaringBitmap/roaring
|
||||||
|
go-fuzz -bin=./roaring-fuzz.zip -workdir=workdir/ -timeout=200
|
||||||
|
|
||||||
|
# Remove any build artifact
|
||||||
|
clean:
|
||||||
|
GOPATH=$(GOPATH) go clean ./...
|
||||||
|
|
||||||
|
# Deletes any intermediate file
|
||||||
|
nuke:
|
||||||
|
rm -rf ./target
|
||||||
|
GOPATH=$(GOPATH) go clean -i ./...
|
||||||
|
|
||||||
|
|
||||||
|
ser:
|
||||||
|
go generate
|
||||||
|
|
||||||
|
cover:
|
||||||
|
go test -coverprofile=coverage.out
|
||||||
|
go tool cover -html=coverage.out
|
||||||
|
|
||||||
|
fetch-real-roaring-datasets:
|
||||||
|
# pull github.com/RoaringBitmap/real-roaring-datasets -> testdata/real-roaring-datasets
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue