mirror of
https://github.com/andatoshiki/shikigrid.git
synced 2026-06-05 19:56:27 +00:00
init: init commit for compelete root soruces of project codebase
This commit is contained in:
commit
2d7d249506
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: jayofelony # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon:
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
.env
|
||||
.idea
|
||||
test-unit
|
||||
test-unit.pub
|
||||
build
|
||||
id_rsa
|
||||
id_rsa.pub
|
||||
key.json
|
||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM golang:alpine as builder
|
||||
|
||||
# ENV GO111MODULE=on
|
||||
|
||||
LABEL maintainer="Anda Toshiki <hello@toshiki.dev>"
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
# download, cache and install deps
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# copy and compiled the app
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shikigrid cmd/shikigrid/main.go
|
||||
|
||||
# start a new stage from scratch
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# copy the prebuilt binary and .env from the builder stage
|
||||
COPY --from=builder /app/shikigrid .
|
||||
COPY --from=builder /app/.env .
|
||||
|
||||
EXPOSE 8666
|
||||
|
||||
CMD ["./shikigrid"]
|
||||
596
LICENSE.md
Normal file
596
LICENSE.md
Normal file
@ -0,0 +1,596 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
==========================
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright © 2007 Free Software Foundation, Inc. <<https://www.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 <https://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
|
||||
<<https://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
|
||||
<<https://www.gnu.org/philosophy/why-not-lgpl.html>>.
|
||||
68
Makefile
Normal file
68
Makefile
Normal file
@ -0,0 +1,68 @@
|
||||
VERSION := $(shell sed -n 's/Version\s*=\s*"\([0-9.]\+\)"/\1/p' version/ver.go | tr -d '\t')
|
||||
|
||||
all: clean
|
||||
@mkdir build
|
||||
@go build -o build/shikigrid cmd/shikigrid/*.go
|
||||
@ls -la build/shikigrid
|
||||
|
||||
install:
|
||||
@cp build/shikigrid /usr/local/bin/
|
||||
@mkdir -p /etc/systemd/system/
|
||||
@mkdir -p /etc/shikigrid/
|
||||
@cp env.example /etc/shikigrid/shikigrid.conf
|
||||
@systemctl daemon-reload
|
||||
|
||||
clean:
|
||||
@rm -rf build
|
||||
|
||||
restart:
|
||||
@service shikigrid restart
|
||||
|
||||
release_files: clean cross_compile_libpcap_arm64 # cross_compile_libpcap_arm
|
||||
@mkdir build
|
||||
@echo building for linux/amd64 ...
|
||||
@CGO_ENABLED=1 CC=x86_64-linux-gnu-gcc GOARCH=amd64 GOOS=linux go build -o build/shikigrid cmd/shikigrid/*.go
|
||||
@openssl dgst -sha256 "build/shikigrid" > "build/shikigrid-amd64.sha256"
|
||||
@zip -j "build/shikigrid-$(VERSION)-amd64.zip" build/shikigrid build/shikigrid-amd64.sha256 > /dev/null
|
||||
@rm -rf build/shikigrid build/shikigrid-amd64.sha256
|
||||
@echo building for linux/armv6l ...
|
||||
@CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOARM=6 GOARCH=arm GOOS=linux go build -o build/shikigrid cmd/shikigrid/*.go
|
||||
@openssl dgst -sha256 "build/shikigrid" > "build/shikigrid-armv6l.sha256"
|
||||
@zip -j "build/shikigrid-$(VERSION)-armv6l.zip" build/shikigrid build/shikigrid-armv6l.sha256 > /dev/null
|
||||
@rm -rf build/shikigrid build/shikigrid-armv6l.sha256
|
||||
@echo building for linux/aarch64 ...
|
||||
@CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc GOARCH=arm64 GOOS=linux go build -o build/shikigrid cmd/shikigrid/*.go
|
||||
@openssl dgst -sha256 "build/shikigrid" > "build/shikigrid-aarch64.sha256"
|
||||
@zip -j "build/shikigrid-$(VERSION)-aarch64.zip" build/shikigrid build/shikigrid-aarch64.sha256 > /dev/null
|
||||
@rm -rf build/shikigrid build/shikigrid-aarch64.sha256
|
||||
@ls -la build
|
||||
|
||||
# requires sudo apt-get install bison flex gcc-arm-linux-gnueabi libpcap0.8 libpcap-dev
|
||||
cross_compile_libpcap_arm:
|
||||
@echo "Cross-compiling libpcap for armv6l..."
|
||||
@wget https://www.tcpdump.org/release/libpcap-1.9.1.tar.gz
|
||||
@tar -zxvf libpcap-1.9.1.tar.gz
|
||||
@cd libpcap-1.9.1 && \
|
||||
export CC=arm-linux-gnueabi-gcc && \
|
||||
./configure --host=arm-linux-gnueabi && \
|
||||
make
|
||||
@echo "Copying cross-compiled libpcap to /usr/lib/arm-linux-gnueabi/"
|
||||
@sudo cp libpcap-1.9.1/libpcap.a /usr/lib/arm-linux-gnueabi/
|
||||
@echo "Clean up..."
|
||||
@rm -rf libpcap-1.9.1 libpcap-1.9.1.tar.gz
|
||||
|
||||
# requires sudo apt-get install bison flex gcc-aarch64-linux-gnu libpcap0.8 libpcap-dev
|
||||
cross_compile_libpcap_arm64:
|
||||
@echo "Cross-compiling libpcap for arm64..."
|
||||
@wget https://www.tcpdump.org/release/libpcap-1.9.1.tar.gz
|
||||
@tar -zxvf libpcap-1.9.1.tar.gz
|
||||
@cd libpcap-1.9.1 && \
|
||||
export CC=aarch64-linux-gnu-gcc && \
|
||||
./configure --host=aarch64-linux-gnu && \
|
||||
make
|
||||
@echo "Copying cross-compiled libpcap to /usr/lib/x86_64-linux-gnu/"
|
||||
@sudo cp libpcap-1.9.1/libpcap.a /usr/lib/aarch64-linux-gnu/
|
||||
@echo "Clean up..."
|
||||
@rm -rf libpcap-1.9.1 libpcap-1.9.1.tar.gz
|
||||
|
||||
.PHONY: cross_compile_libpcap_arm cross_compile_libpcap_arm64
|
||||
89
api/auth.go
Normal file
89
api/auth.go
Normal file
@ -0,0 +1,89 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTokenClaims = errors.New("can't extract claims from jwt token")
|
||||
ErrTokenInvalid = errors.New("jwt token not valid")
|
||||
ErrTokenExpired = errors.New("jwt token expired")
|
||||
ErrTokenIncomplete = errors.New("jwt token is missing required fields")
|
||||
ErrTokenUnauthorized = errors.New("jwt token authorized field is false (?!)")
|
||||
)
|
||||
|
||||
func validateToken(header string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(header, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(os.Getenv("API_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, ErrTokenClaims
|
||||
} else if !token.Valid {
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
|
||||
required := []string{
|
||||
"expires_at",
|
||||
"authorized",
|
||||
"unit_id",
|
||||
"unit_ident",
|
||||
}
|
||||
for _, req := range required {
|
||||
if _, found := claims[req]; !found {
|
||||
return nil, ErrTokenIncomplete
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("%+v", claims)
|
||||
|
||||
if expiresAt, err := time.Parse(time.RFC3339, claims["expires_at"].(string)); err != nil {
|
||||
return nil, ErrTokenExpired
|
||||
} else if expiresAt.Before(time.Now()) {
|
||||
return nil, ErrTokenExpired
|
||||
} else if claims["authorized"].(bool) != true {
|
||||
return nil, ErrTokenUnauthorized
|
||||
}
|
||||
return claims, err
|
||||
}
|
||||
|
||||
func Authenticate(w http.ResponseWriter, r *http.Request) *models.Unit {
|
||||
client := clientIP(r)
|
||||
tokenHeader := reqToken(r)
|
||||
if tokenHeader == "" {
|
||||
log.Debug("unauthenticated request from %s", client)
|
||||
ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
claims, err := validateToken(tokenHeader)
|
||||
if err != nil {
|
||||
log.Debug("token error for %s: %v", client, err)
|
||||
ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("claims[unit_id] = %+v", claims["unit_id"])
|
||||
unit := models.FindUnit(uint(claims["unit_id"].(float64)))
|
||||
if unit == nil {
|
||||
log.Warning("client %s authenticated with unknown claims '%v'", client, claims)
|
||||
ERROR(w, http.StatusUnauthorized, ErrUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
return unit
|
||||
}
|
||||
263
api/client.go
Normal file
263
api/client.go
Normal file
@ -0,0 +1,263 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"github.com/andatoshiki/shikigrid/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
ClientTimeout = 60
|
||||
ClientKeepalive = 30
|
||||
ClientTokenFile = "/tmp/shikigrid-api-enrollment.json"
|
||||
Endpoint = ""
|
||||
)
|
||||
|
||||
//const (
|
||||
// Endpoint
|
||||
//)
|
||||
|
||||
type Client struct {
|
||||
sync.Mutex
|
||||
|
||||
cli *http.Client
|
||||
keys *crypto.KeyPair
|
||||
token string
|
||||
tokenAt time.Time
|
||||
data map[string]interface{}
|
||||
hostname string
|
||||
}
|
||||
|
||||
func NewClient(keys *crypto.KeyPair, endpoint string, hostname string) *Client {
|
||||
|
||||
t := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: time.Duration(ClientTimeout) * time.Second,
|
||||
KeepAlive: time.Duration(ClientKeepalive) * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: time.Duration(ClientTimeout) * time.Second,
|
||||
ResponseHeaderTimeout: time.Duration(ClientTimeout) * time.Second,
|
||||
ExpectContinueTimeout: 4 * time.Second,
|
||||
}
|
||||
|
||||
cli := &Client{
|
||||
cli: &http.Client{
|
||||
Transport: t,
|
||||
Timeout: time.Duration(ClientTimeout) * time.Second,
|
||||
},
|
||||
keys: keys,
|
||||
data: make(map[string]interface{}),
|
||||
hostname: hostname,
|
||||
}
|
||||
|
||||
Endpoint = endpoint
|
||||
|
||||
if info, err := os.Stat(ClientTokenFile); err == nil {
|
||||
if time.Since(info.ModTime()) < models.TokenTTL {
|
||||
log.Debug("loading token from %s ...", ClientTokenFile)
|
||||
var data map[string]interface{}
|
||||
if raw, err := os.ReadFile(ClientTokenFile); err == nil {
|
||||
if err := json.Unmarshal(raw, &data); err == nil {
|
||||
cli.token = data["token"].(string)
|
||||
cli.tokenAt = info.ModTime()
|
||||
log.Debug("token: %s", cli.token)
|
||||
} else {
|
||||
log.Warning("error decoding %s: %v", ClientTokenFile, err)
|
||||
}
|
||||
} else {
|
||||
log.Warning("error reading %s: %v", ClientTokenFile, err)
|
||||
}
|
||||
} else {
|
||||
log.Debug("token in %s is expired", ClientTokenFile)
|
||||
}
|
||||
}
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func (c *Client) enroll() error {
|
||||
|
||||
hostname := c.hostname
|
||||
if hostname == "" {
|
||||
hostname = utils.Hostname()
|
||||
}
|
||||
identity := fmt.Sprintf("%s@%s", hostname, c.keys.FingerprintHex)
|
||||
|
||||
log.Debug("refreshing api token as %s ...", identity)
|
||||
|
||||
signature, err := c.keys.SignMessage([]byte(identity))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signature64 := base64.StdEncoding.EncodeToString(signature)
|
||||
pubKeyPEM64 := base64.StdEncoding.EncodeToString(c.keys.PublicPEM)
|
||||
|
||||
log.Debug("SIGN(%s) = %s", identity, signature64)
|
||||
|
||||
enrollment := map[string]interface{}{
|
||||
"identity": identity,
|
||||
"public_key": pubKeyPEM64,
|
||||
"signature": signature64,
|
||||
"data": c.data,
|
||||
}
|
||||
|
||||
obj, err := c.request("POST", "/unit/enroll", enrollment, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.tokenAt = time.Now()
|
||||
c.token = obj["token"].(string)
|
||||
log.Debug("new token: %s", c.token)
|
||||
|
||||
if raw, err := json.Marshal(obj); err == nil {
|
||||
log.Debug("saving token to %s ...", ClientTokenFile)
|
||||
if err = os.WriteFile(ClientTokenFile, raw, 0644); err != nil {
|
||||
log.Warning("error saving token to %s: %v", ClientTokenFile, err)
|
||||
}
|
||||
} else {
|
||||
log.Warning("error encoding token: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) request(method string, path string, data interface{}, auth bool) (map[string]interface{}, error) {
|
||||
url := fmt.Sprintf("%s%s", Endpoint, path)
|
||||
err := (error)(nil)
|
||||
started := time.Now()
|
||||
defer func() {
|
||||
if err == nil {
|
||||
log.Debug("%s %s (%s)", method, url, time.Since(started))
|
||||
} else {
|
||||
log.Error("%s %s (%s) %v", method, url, time.Since(started), err)
|
||||
}
|
||||
}()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if data != nil {
|
||||
if err = json.NewEncoder(buf).Encode(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if auth {
|
||||
if time.Since(c.tokenAt) >= models.TokenTTL {
|
||||
if err := c.enroll(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token))
|
||||
}
|
||||
|
||||
res, err := c.cli.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var obj map[string]interface{}
|
||||
if err = json.Unmarshal(body, &obj); err != nil {
|
||||
log.Debug(fmt.Sprintf("Error Unmarshalling json body from request: %v", body))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if res.StatusCode == 401 {
|
||||
if err := c.enroll(); err != nil {
|
||||
log.Warning("error token expired during operation: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
log.Warning("token expired, re-enroll success")
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
err = fmt.Errorf("%d %s", res.StatusCode, obj["error"])
|
||||
}
|
||||
|
||||
return obj, err
|
||||
}
|
||||
|
||||
func (c *Client) SetData(newData map[string]interface{}) map[string]interface{} {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
for key, val := range newData {
|
||||
if val == nil {
|
||||
delete(c.data, key)
|
||||
} else {
|
||||
c.data[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
return c.data
|
||||
}
|
||||
|
||||
func (c *Client) Data() map[string]interface{} {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.data
|
||||
}
|
||||
|
||||
func (c *Client) Request(method string, path string, data interface{}, auth bool) (map[string]interface{}, error) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
return c.request(method, path, data, auth)
|
||||
}
|
||||
|
||||
func (c *Client) Get(path string, auth bool) (map[string]interface{}, error) {
|
||||
return c.Request("GET", path, nil, auth)
|
||||
}
|
||||
|
||||
func (c *Client) Post(path string, what interface{}, auth bool) (map[string]interface{}, error) {
|
||||
return c.Request("POST", path, what, auth)
|
||||
}
|
||||
|
||||
func (c *Client) PagedUnits(page int) (map[string]interface{}, error) {
|
||||
return c.Get(fmt.Sprintf("/units/?p=%d", page), false)
|
||||
}
|
||||
|
||||
func (c *Client) Unit(fingerprint string) (map[string]interface{}, error) {
|
||||
return c.Get(fmt.Sprintf("/unit/%s", fingerprint), false)
|
||||
}
|
||||
|
||||
func (c *Client) ReportAP(report interface{}) (map[string]interface{}, error) {
|
||||
return c.Post("/unit/report/ap", report, true)
|
||||
}
|
||||
|
||||
func (c *Client) Inbox(page int) (map[string]interface{}, error) {
|
||||
return c.Get(fmt.Sprintf("/unit/inbox/?p=%d", page), true)
|
||||
}
|
||||
|
||||
func (c *Client) InboxMessage(id int) (map[string]interface{}, error) {
|
||||
return c.Get(fmt.Sprintf("/unit/inbox/%d", id), true)
|
||||
}
|
||||
|
||||
func (c *Client) MarkInboxMessage(id int, mark string) (map[string]interface{}, error) {
|
||||
return c.Get(fmt.Sprintf("/unit/inbox/%d/%s", id, mark), true)
|
||||
}
|
||||
|
||||
func (c *Client) SendMessageTo(fingerprint string, msg Message) error {
|
||||
_, err := c.Post(fmt.Sprintf("/unit/%s/inbox", fingerprint), msg, true)
|
||||
return err
|
||||
}
|
||||
23
api/cors.go
Normal file
23
api/cors.go
Normal file
@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/go-chi/cors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
cors := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
})
|
||||
return cors.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||
w.Header().Add("Referrer-Policy", "same-origin")
|
||||
next.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
6
api/message.go
Normal file
6
api/message.go
Normal file
@ -0,0 +1,6 @@
|
||||
package api
|
||||
|
||||
type Message struct {
|
||||
Data string `json:"data"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
33
api/peer_data.go
Normal file
33
api/peer_data.go
Normal file
@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// PeerGetData GET /api/v1/data
|
||||
func (api *API) PeerGetData(w http.ResponseWriter, r *http.Request) {
|
||||
JSON(w, http.StatusOK, api.Client.Data())
|
||||
}
|
||||
|
||||
// PeerSetData POST /api/v1/data
|
||||
func (api *API) PeerSetData(w http.ResponseWriter, r *http.Request) {
|
||||
var newData map[string]interface{}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("%s", body)
|
||||
|
||||
if err = json.Unmarshal(body, &newData); err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, api.Client.SetData(newData))
|
||||
}
|
||||
195
api/peer_inbox.go
Normal file
195
api/peer_inbox.go
Normal file
@ -0,0 +1,195 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyMessage = errors.New("empty message body")
|
||||
ErrSenderNotFound = errors.New("sender not found")
|
||||
)
|
||||
|
||||
// PeerGetInbox /api/v1/inbox/
|
||||
func (api *API) PeerGetInbox(w http.ResponseWriter, r *http.Request) {
|
||||
page, err := pageNum(r)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := api.Client.Inbox(page)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, obj)
|
||||
}
|
||||
|
||||
func (api *API) InboxMessage(id int) (map[string]interface{}, int, error) {
|
||||
message, err := api.Client.InboxMessage(id)
|
||||
if err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
sender, found := message["sender"]
|
||||
if !found {
|
||||
return nil, http.StatusNotFound, ErrSenderNotFound
|
||||
}
|
||||
|
||||
fingerprint, ok := sender.(string)
|
||||
if !ok {
|
||||
return nil, http.StatusUnprocessableEntity, ErrSenderNotFound
|
||||
}
|
||||
|
||||
unit, err := api.Client.Unit(fingerprint)
|
||||
if err != nil {
|
||||
return nil, http.StatusNotFound, err
|
||||
}
|
||||
|
||||
srcKeys, err := crypto.FromPublicPEM(unit["public_key"].(string))
|
||||
if err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(message["data"].(string))
|
||||
if err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(message["signature"].(string))
|
||||
if err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
log.Info("verifying message from %s ...", fingerprint)
|
||||
|
||||
if err := srcKeys.VerifyMessage(data, signature); err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
log.Info("decrypting message from %s ...", fingerprint)
|
||||
|
||||
clearText, err := api.Keys.Decrypt(data)
|
||||
if err != nil {
|
||||
return nil, http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
message["data"] = clearText
|
||||
|
||||
return message, 0, nil
|
||||
}
|
||||
|
||||
// /api/v1/inbox/<msg_id>
|
||||
func (api *API) PeerGetInboxMessage(w http.ResponseWriter, r *http.Request) {
|
||||
msgIDParam := chi.URLParam(r, "msg_id")
|
||||
msgID, err := strconv.Atoi(msgIDParam)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
message, status, err := api.InboxMessage(msgID)
|
||||
if err != nil {
|
||||
ERROR(w, status, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, message)
|
||||
}
|
||||
|
||||
// /api/v1/inbox/<msg_id>/<mark>
|
||||
func (api *API) PeerMarkInboxMessage(w http.ResponseWriter, r *http.Request) {
|
||||
markAs := chi.URLParam(r, "mark")
|
||||
msgIDParam := chi.URLParam(r, "msg_id")
|
||||
msgID, err := strconv.Atoi(msgIDParam)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := api.Client.MarkInboxMessage(msgID, markAs)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, obj)
|
||||
}
|
||||
|
||||
func (api *API) SendMessage(fingerprint string, cleartext []byte) (int, error) {
|
||||
unit, err := api.Client.Unit(fingerprint)
|
||||
if err != nil {
|
||||
return http.StatusNotFound, err
|
||||
}
|
||||
|
||||
unitKeys, err := crypto.FromPublicPEM(unit["public_key"].(string))
|
||||
if err != nil {
|
||||
log.Error("error parsing public key of %s: %v", fingerprint, err)
|
||||
return http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
messageBody, err := api.Keys.EncryptFor(cleartext, unitKeys.Public)
|
||||
if err != nil {
|
||||
log.Error("error encrypting message for %s: %v", fingerprint, err)
|
||||
return http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
messageSize := len(messageBody)
|
||||
if messageSize == 0 {
|
||||
return http.StatusUnprocessableEntity, ErrEmptyMessage
|
||||
} else if messageSize > models.MessageDataMaxSize {
|
||||
err := fmt.Errorf("max message signature size is %d", models.MessageSignatureMaxSize)
|
||||
return http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
log.Info("signing encrypted message of %d bytes for %s ...", messageSize, fingerprint)
|
||||
|
||||
signature, err := api.Keys.SignMessage(messageBody)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
return http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
msg := Message{
|
||||
Signature: base64.StdEncoding.EncodeToString(signature),
|
||||
Data: base64.StdEncoding.EncodeToString(messageBody),
|
||||
}
|
||||
|
||||
if err := api.Client.SendMessageTo(fingerprint, msg); err != nil {
|
||||
log.Error("%v", err)
|
||||
return http.StatusUnprocessableEntity, err
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// POST /api/v1/unit/<fingerprint>/inbox
|
||||
func (api *API) PeerSendMessageTo(w http.ResponseWriter, r *http.Request) {
|
||||
cleartextMessage, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error("error reading request body: %v", err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
fingerprint := chi.URLParam(r, "fingerprint")
|
||||
status, err := api.SendMessage(fingerprint, cleartextMessage)
|
||||
if err != nil {
|
||||
ERROR(w, status, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
21
api/peer_list_units.go
Normal file
21
api/peer_list_units.go
Normal file
@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) PeerListUnits(w http.ResponseWriter, r *http.Request) {
|
||||
page, err := pageNum(r)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := api.Client.PagedUnits(page)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, obj)
|
||||
}
|
||||
110
api/peer_mesh.go
Normal file
110
api/peer_mesh.go
Normal file
@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/andatoshiki/shikigrid/mesh"
|
||||
)
|
||||
|
||||
// PeerGetPeers GET /api/v1/mesh/peers
|
||||
func (api *API) PeerGetPeers(w http.ResponseWriter, r *http.Request) {
|
||||
peers := make([]*mesh.Peer, 0)
|
||||
mesh.Peers.Range(func(key, value interface{}) bool {
|
||||
peers = append(peers, value.(*mesh.Peer))
|
||||
return true
|
||||
})
|
||||
|
||||
// closer first
|
||||
sort.Slice(peers, func(i, j int) bool {
|
||||
return peers[i].RSSI > peers[j].RSSI
|
||||
})
|
||||
|
||||
JSON(w, http.StatusOK, peers)
|
||||
}
|
||||
|
||||
// PeerGetMemory GET /api/v1/mesh/memory
|
||||
func (api *API) PeerGetMemory(w http.ResponseWriter, r *http.Request) {
|
||||
peers := api.Mesh.Memory()
|
||||
// higher number of encounters first
|
||||
sort.Slice(peers, func(i, j int) bool {
|
||||
return peers[i].Encounters > peers[j].Encounters
|
||||
})
|
||||
JSON(w, http.StatusOK, peers)
|
||||
}
|
||||
|
||||
// PeerGetMemoryOf GET /api/v1/mesh/memory/<fingerprint>
|
||||
func (api *API) PeerGetMemoryOf(w http.ResponseWriter, r *http.Request) {
|
||||
fingerprint := chi.URLParam(r, "fingerprint")
|
||||
peer := api.Mesh.MemoryOf(fingerprint)
|
||||
if peer == nil {
|
||||
ERROR(w, http.StatusNotFound, ErrEmpty)
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusOK, peer)
|
||||
}
|
||||
|
||||
// PeerSetSignaling GET /api/v1/mesh/<status>
|
||||
func (api *API) PeerSetSignaling(w http.ResponseWriter, r *http.Request) {
|
||||
status := chi.URLParam(r, "status")
|
||||
|
||||
if status == "enabled" || status == "true" {
|
||||
api.Peer.Advertise(true)
|
||||
} else if status == "disabled" || status == "false" {
|
||||
api.Peer.Advertise(false)
|
||||
} else {
|
||||
ERROR(w, http.StatusNotFound, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
// PeerGetMeshData GET /api/v1/mesh/data
|
||||
func (api *API) PeerGetMeshData(w http.ResponseWriter, r *http.Request) {
|
||||
JSON(w, http.StatusOK, api.Peer.Data())
|
||||
}
|
||||
|
||||
// PeerSetMeshData POST /api/v1/mesh/data
|
||||
func (api *API) PeerSetMeshData(w http.ResponseWriter, r *http.Request) {
|
||||
var newData map[string]interface{}
|
||||
|
||||
if api.Peer.ForceDisabled == true {
|
||||
api.Peer.Advertise(false)
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true, // this should be changed later when shikigotchi can handle shikigrid being force advertise disabled
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("%s", body)
|
||||
|
||||
if err = json.Unmarshal(body, &newData); err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
// this makes sure that the shikigrid server receives advertisements
|
||||
api.Client.SetData(map[string]interface{}{
|
||||
"advertisement": newData,
|
||||
})
|
||||
|
||||
// update mesh advertisement data
|
||||
api.Peer.SetData(newData)
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
34
api/peer_report.go
Normal file
34
api/peer_report.go
Normal file
@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// PeerReportAP POST /api/v1/report/ap
|
||||
func (api *API) PeerReportAP(w http.ResponseWriter, r *http.Request) {
|
||||
var report apReport
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("%s", body)
|
||||
|
||||
if err = json.Unmarshal(body, &report); err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := api.Client.ReportAP(report)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, obj)
|
||||
}
|
||||
45
api/setup.go
Normal file
45
api/setup.go
Normal file
@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/mesh"
|
||||
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
Router *chi.Mux
|
||||
Keys *crypto.KeyPair
|
||||
Peer *mesh.Peer
|
||||
Mesh *mesh.Router
|
||||
Client *Client
|
||||
}
|
||||
|
||||
func Setup(keys *crypto.KeyPair, peer *mesh.Peer, router *mesh.Router, Endpoint string, Hostname string) (err error, api *API) {
|
||||
api = &API{
|
||||
Router: chi.NewRouter(),
|
||||
Keys: keys,
|
||||
Peer: peer,
|
||||
Mesh: router,
|
||||
Client: NewClient(keys, Endpoint, Hostname),
|
||||
}
|
||||
|
||||
api.Router.Use(CORS)
|
||||
if api.Keys == nil {
|
||||
api.setupServerRoutes()
|
||||
} else {
|
||||
api.setupPeerRoutes()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) Run(addr string) {
|
||||
log.Info("shikigrid api starting on %s ...", addr)
|
||||
log.Fatal("%v", http.ListenAndServe(addr, api.Router))
|
||||
}
|
||||
62
api/setup_peer.go
Normal file
62
api/setup_peer.go
Normal file
@ -0,0 +1,62 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func (api *API) setupPeerRoutes() {
|
||||
log.Debug("registering peer api ...")
|
||||
|
||||
api.Router.Route("/api", func(r chi.Router) {
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/mesh", func(r chi.Router) {
|
||||
// GET /api/v1/mesh/peers
|
||||
r.Get("/peers", api.PeerGetPeers)
|
||||
|
||||
r.Route("/memory", func(r chi.Router) {
|
||||
// GET /api/v1/mesh/memory
|
||||
r.Get("/", api.PeerGetMemory)
|
||||
// GET /api/v1/mesh/memory/<fingerprint>
|
||||
r.Get("/{fingerprint:[a-fA-F0-9]+}", api.PeerGetMemoryOf)
|
||||
})
|
||||
|
||||
// GET /api/v1/mesh/<status>
|
||||
r.Get("/{status:[a-z]+}", api.PeerSetSignaling)
|
||||
|
||||
// GET /api/v1/mesh/data
|
||||
r.Get("/data", api.PeerGetMeshData)
|
||||
// POST /api/v1/mesh/data
|
||||
r.Post("/data", api.PeerSetMeshData)
|
||||
})
|
||||
|
||||
// GET /api/v1/data
|
||||
r.Post("/data", api.PeerGetData)
|
||||
// POST /api/v1/data
|
||||
r.Post("/data", api.PeerSetData)
|
||||
|
||||
r.Route("/report", func(r chi.Router) {
|
||||
// POST /api/v1/report/ap
|
||||
r.Post("/ap", api.PeerReportAP)
|
||||
})
|
||||
r.Route("/inbox", func(r chi.Router) {
|
||||
// GET /api/v1/inbox/
|
||||
r.Get("/", api.PeerGetInbox)
|
||||
r.Route("/{msg_id:[0-9]+}", func(r chi.Router) {
|
||||
// GET /api/v1/inbox/<msg_id>
|
||||
r.Get("/", api.PeerGetInboxMessage)
|
||||
// GET /api/v1/inbox/<msg_id>/<mark>
|
||||
r.Get("/{mark:[a-z]+}", api.PeerMarkInboxMessage)
|
||||
})
|
||||
})
|
||||
r.Route("/unit", func(r chi.Router) {
|
||||
// POST /api/v1/unit/<fingerprint>/inbox
|
||||
r.Post("/{fingerprint:[a-fA-F0-9]+}/inbox", api.PeerSendMessageTo)
|
||||
})
|
||||
r.Route("/units", func(r chi.Router) {
|
||||
// GET /api/v1/units/
|
||||
r.Get("/", api.PeerListUnits)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
55
api/setup_server.go
Normal file
55
api/setup_server.go
Normal file
@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func cached(seconds int, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
|
||||
w.Header().Add("Expires", fmt.Sprintf("%d", seconds))
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) setupServerRoutes() {
|
||||
log.Debug("registering server api ...")
|
||||
|
||||
api.Router.Route("/api", func(r chi.Router) {
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/units", func(r chi.Router) {
|
||||
// GET /api/v1/units/
|
||||
r.Get("/", cached(600, api.ListUnits))
|
||||
// GET /api/v1/units/by_country
|
||||
r.Get("/by_country", cached(600, api.UnitsByCountry))
|
||||
})
|
||||
r.Route("/unit", func(r chi.Router) {
|
||||
// GET /api/v1/unit/<fingerprint>
|
||||
r.Get("/{fingerprint:[a-fA-F0-9]+}", cached(600, api.ShowUnit))
|
||||
r.Route("/inbox", func(r chi.Router) {
|
||||
// GET /api/v1/unit/inbox/
|
||||
r.Get("/", api.GetInbox)
|
||||
r.Route("/{msg_id:[0-9]+}", func(r chi.Router) {
|
||||
// GET /api/v1/unit/inbox/<msg_id>
|
||||
r.Get("/", api.GetInboxMessage)
|
||||
// GET /api/v1/unit/inbox/<msg_id>/<mark>
|
||||
r.Get("/{mark:[a-z]+}", api.MarkInboxMessage)
|
||||
})
|
||||
})
|
||||
// POST /api/v1/unit/<fingerprint>/inbox
|
||||
r.Post("/{fingerprint:[a-fA-F0-9]+}/inbox", api.SendMessageTo)
|
||||
// POST /api/v1/unit/enroll
|
||||
r.Post("/enroll", api.UnitEnroll)
|
||||
r.Route("/report", func(r chi.Router) {
|
||||
// POST /api/v1/unit/report/ap
|
||||
r.Post("/ap", api.UnitReportAP)
|
||||
// POST /api/v1/unit/report/aps
|
||||
r.Post("/aps", api.UnitReportMultipleAP)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
60
api/unit_enroll.go
Normal file
60
api/unit_enroll.go
Normal file
@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) readEnrollment(w http.ResponseWriter, r *http.Request) (error, models.EnrollmentRequest) {
|
||||
var enroll models.EnrollmentRequest
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return err, enroll
|
||||
}
|
||||
|
||||
log.Debug("%s", body)
|
||||
|
||||
enroll.Address = clientIP(r)
|
||||
enroll.Country = r.Header.Get("CF-IPCountry")
|
||||
|
||||
if err = json.Unmarshal(body, &enroll); err != nil {
|
||||
log.Warning("error while reading enrollment request from %s: %v", enroll.Address, err)
|
||||
log.Debug("%s", body)
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return err, enroll
|
||||
}
|
||||
|
||||
if err = enroll.Validate(); err != nil {
|
||||
log.Warning("error while validating enrollment request from %s: %v", enroll.Address, err)
|
||||
log.Debug("%s", body)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return err, enroll
|
||||
}
|
||||
|
||||
return nil, enroll
|
||||
}
|
||||
|
||||
func (api *API) UnitEnroll(w http.ResponseWriter, r *http.Request) {
|
||||
err, enroll := api.readEnrollment(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err, unit := models.EnrollUnit(enroll)
|
||||
if err != nil {
|
||||
log.Error("%v", err)
|
||||
ERROR(w, http.StatusInternalServerError, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("unit %s enrolled: id:%d address:%s", unit.Identity(), unit.ID, unit.Address)
|
||||
|
||||
JSON(w, http.StatusOK, map[string]string{
|
||||
"token": unit.Token,
|
||||
})
|
||||
}
|
||||
222
api/unit_inbox.go
Normal file
222
api/unit_inbox.go
Normal file
@ -0,0 +1,222 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRecNotFound = errors.New("recipient not found")
|
||||
ErrMessageNotFound = errors.New("message not found")
|
||||
ErrInvalidKey = errors.New("invalid public key")
|
||||
ErrInvalidSignature = errors.New("can't verify signature")
|
||||
ErrDecoding = errors.New("error decoding data")
|
||||
)
|
||||
|
||||
func (api *API) GetInbox(w http.ResponseWriter, r *http.Request) {
|
||||
unit := Authenticate(w, r)
|
||||
if unit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
page, err := pageNum(r)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
messages, total, pages := unit.GetPagedInbox(page)
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"records": total,
|
||||
"pages": pages,
|
||||
"messages": messages,
|
||||
})
|
||||
}
|
||||
|
||||
// we need this because the models.Message structure doesn't not export data and signature for fast listing.
|
||||
type fullMessage struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
SeenAt *time.Time `json:"seen_at"`
|
||||
Sender string `json:"sender"`
|
||||
SenderName string `json:"sender_name"`
|
||||
Data string `json:"data"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
func (api *API) GetInboxMessage(w http.ResponseWriter, r *http.Request) {
|
||||
unit := Authenticate(w, r)
|
||||
if unit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
msgIDParam := chi.URLParam(r, "msg_id")
|
||||
msgID, err := strconv.Atoi(msgIDParam)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
} else if message := unit.GetInboxMessage(msgID); message == nil {
|
||||
ERROR(w, http.StatusNotFound, ErrMessageNotFound)
|
||||
return
|
||||
} else {
|
||||
JSON(w, http.StatusOK, fullMessage{
|
||||
ID: message.ID,
|
||||
CreatedAt: message.CreatedAt,
|
||||
UpdatedAt: message.UpdatedAt,
|
||||
DeletedAt: message.DeletedAt,
|
||||
SeenAt: message.SeenAt,
|
||||
Sender: message.Sender,
|
||||
SenderName: message.SenderName,
|
||||
Data: message.Data,
|
||||
Signature: message.Signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) MarkInboxMessage(w http.ResponseWriter, r *http.Request) {
|
||||
unit := Authenticate(w, r)
|
||||
if unit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
markAs := chi.URLParam(r, "mark")
|
||||
msgIDParam := chi.URLParam(r, "msg_id")
|
||||
msgID, err := strconv.Atoi(msgIDParam)
|
||||
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
} else if message := unit.GetInboxMessage(msgID); message == nil {
|
||||
ERROR(w, http.StatusNotFound, ErrMessageNotFound)
|
||||
return
|
||||
} else if markAs == "seen" {
|
||||
if err := models.UpdateFields(message, map[string]interface{}{"seen_at": &now}).Error; err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
} else if markAs == "unseen" {
|
||||
if err := models.UpdateFields(message, map[string]interface{}{"seen_at": nil}).Error; err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
} else if markAs == "deleted" {
|
||||
if err := models.UpdateFields(message, map[string]interface{}{"deleted_at": &now}).Error; err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
} else if markAs == "restored" {
|
||||
if err := models.UpdateFields(message, map[string]interface{}{"deleted_at": nil}).Error; err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ERROR(w, http.StatusNotFound, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]bool{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) SendMessageTo(w http.ResponseWriter, r *http.Request) {
|
||||
// authenticate source unit
|
||||
srcUnit := Authenticate(w, r)
|
||||
if srcUnit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get dest unit by fingerprint
|
||||
dstUnitFingerprint := chi.URLParam(r, "fingerprint")
|
||||
dstUnit := models.FindUnitByFingerprint(dstUnitFingerprint)
|
||||
if dstUnit == nil {
|
||||
ERROR(w, http.StatusNotFound, ErrRecNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// read the message and signature from the source unit
|
||||
client := clientIP(r)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
var message Message
|
||||
if err = json.Unmarshal(body, &message); err != nil {
|
||||
log.Debug("error while decoding message from %s: %v", srcUnit.Identity(), err)
|
||||
log.Debug("%s", body)
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.ValidateMessage(message.Data, message.Signature); err != nil {
|
||||
log.Warning("client %s sent a broken message structure: %v", srcUnit.Identity(), err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
// parse source unit key
|
||||
srcKeys, err := crypto.FromPublicPEM(srcUnit.PublicKey)
|
||||
if err != nil {
|
||||
log.Warning("error decoding key from %s: %v", srcUnit.Identity(), err)
|
||||
log.Debug("%s", srcUnit.PublicKey)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrInvalidKey)
|
||||
return
|
||||
}
|
||||
|
||||
// decode data, signature and verify SIGN(SHA256(data))
|
||||
data, err := base64.StdEncoding.DecodeString(message.Data)
|
||||
if err != nil {
|
||||
log.Warning("error decoding message from %s: %v", srcUnit.Identity(), err)
|
||||
log.Debug("%s", message.Data)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrDecoding)
|
||||
return
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(message.Signature)
|
||||
if err != nil {
|
||||
log.Warning("error decoding signature from %s: %v", srcUnit.Identity(), err)
|
||||
log.Debug("%s", message.Signature)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrDecoding)
|
||||
return
|
||||
}
|
||||
|
||||
if err := srcKeys.VerifyMessage(data, signature); err != nil {
|
||||
log.Warning("error verifying signature from %s: %v", srcUnit.Identity(), err)
|
||||
log.Debug("%s", message.Signature)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrInvalidSignature)
|
||||
return
|
||||
}
|
||||
|
||||
msg := models.Message{
|
||||
SenderID: srcUnit.ID,
|
||||
Sender: srcUnit.Fingerprint,
|
||||
SenderName: srcUnit.Name,
|
||||
ReceiverID: dstUnit.ID,
|
||||
Data: message.Data,
|
||||
Signature: message.Signature,
|
||||
}
|
||||
|
||||
if err := models.Create(&msg).Error; err != nil {
|
||||
log.Warning("error creating msg %v from %s: %v", msg, client, err)
|
||||
ERROR(w, http.StatusInternalServerError, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
108
api/unit_report.go
Normal file
108
api/unit_report.go
Normal file
@ -0,0 +1,108 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type apReport struct {
|
||||
ESSID string `json:"essid"`
|
||||
BSSID string `json:"bssid"`
|
||||
}
|
||||
|
||||
func (api *API) unitReport(client string, unit *models.Unit, ap apReport) error {
|
||||
if parsed, err := net.ParseMAC(ap.BSSID); err != nil {
|
||||
return fmt.Errorf("error while parsing wifi ap bssid %s from %s: %v", ap.BSSID, client, err)
|
||||
} else {
|
||||
// normalize
|
||||
ap.BSSID = parsed.String()
|
||||
}
|
||||
|
||||
if existing := unit.FindAccessPoint(ap.ESSID, ap.BSSID); existing == nil {
|
||||
log.Debug("unit %s (%s %s) reporting new wifi access point %v", unit.Identity(), unit.Address,
|
||||
unit.Country, ap)
|
||||
|
||||
newAP := models.AccessPoint{
|
||||
Name: ap.ESSID,
|
||||
Mac: ap.BSSID,
|
||||
UnitID: unit.ID,
|
||||
}
|
||||
|
||||
if err := models.Create(&newAP).Error; err != nil {
|
||||
return fmt.Errorf("error creating ap %v: %v", newAP, err)
|
||||
}
|
||||
} else if err := models.Update(existing).Error; err != nil {
|
||||
return fmt.Errorf("error updating ap %v: %v", existing, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) UnitReportAP(w http.ResponseWriter, r *http.Request) {
|
||||
unit := Authenticate(w, r)
|
||||
if unit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
client := clientIP(r)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
var ap apReport
|
||||
if err = json.Unmarshal(body, &ap); err != nil {
|
||||
log.Warning("error while reading wifi ap from %s: %v", client, err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
if err := api.unitReport(client, unit, ap); err != nil {
|
||||
log.Warning("%v", err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) UnitReportMultipleAP(w http.ResponseWriter, r *http.Request) {
|
||||
unit := Authenticate(w, r)
|
||||
if unit == nil {
|
||||
return
|
||||
}
|
||||
|
||||
client := clientIP(r)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
var aps []apReport
|
||||
if err = json.Unmarshal(body, &aps); err != nil {
|
||||
log.Warning("error while reading wifi ap list from %s: %v", client, err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ap := range aps {
|
||||
if err := api.unitReport(client, unit, ap); err != nil {
|
||||
log.Warning("%v", err)
|
||||
ERROR(w, http.StatusUnprocessableEntity, ErrEmpty)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
17
api/unit_show.go
Normal file
17
api/unit_show.go
Normal file
@ -0,0 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) ShowUnit(w http.ResponseWriter, r *http.Request) {
|
||||
unitFingerprint := chi.URLParam(r, "fingerprint")
|
||||
if unit := models.FindUnitByFingerprint(unitFingerprint); unit == nil {
|
||||
ERROR(w, http.StatusNotFound, ErrEmpty)
|
||||
return
|
||||
} else {
|
||||
JSON(w, http.StatusOK, unit)
|
||||
}
|
||||
}
|
||||
33
api/units_list.go
Normal file
33
api/units_list.go
Normal file
@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) ListUnits(w http.ResponseWriter, r *http.Request) {
|
||||
page, err := pageNum(r)
|
||||
if err != nil {
|
||||
ERROR(w, http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
|
||||
units, total, pages := models.GetPagedUnits(page)
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"records": total,
|
||||
"pages": pages,
|
||||
"units": units,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) UnitsByCountry(w http.ResponseWriter, r *http.Request) {
|
||||
if results, err := models.GetUnitsByCountry(); err != nil {
|
||||
log.Warning("error getting units by country: %v", err)
|
||||
ERROR(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
} else {
|
||||
JSON(w, http.StatusOK, results)
|
||||
}
|
||||
}
|
||||
78
api/utils.go
Normal file
78
api/utils.go
Normal file
@ -0,0 +1,78 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmpty = errors.New("")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
func clientIP(r *http.Request) string {
|
||||
address := strings.Split(r.RemoteAddr, ":")[0]
|
||||
if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
|
||||
address = forwardedFor
|
||||
}
|
||||
// https://support.cloudflare.com/hc/en-us/articles/206776727-What-is-True-Client-IP-
|
||||
if trueClient := r.Header.Get("True-Client-IP"); trueClient != "" {
|
||||
address = trueClient
|
||||
}
|
||||
// handle multiple IPs case
|
||||
return strings.Trim(strings.Split(address, ",")[0], " ")
|
||||
}
|
||||
|
||||
func reqToken(r *http.Request) string {
|
||||
keys := r.URL.Query()
|
||||
token := keys.Get("token")
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
bearerToken := r.Header.Get("Authorization")
|
||||
if parts := strings.Split(bearerToken, " "); len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func pageNum(r *http.Request) (int, error) {
|
||||
pageParam := r.URL.Query().Get("p")
|
||||
if pageParam == "" {
|
||||
pageParam = "1"
|
||||
}
|
||||
return strconv.Atoi(pageParam)
|
||||
}
|
||||
|
||||
func JSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||
js, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
if sent, err := w.Write(js); err != nil {
|
||||
log.Error("error sending response: %v", err)
|
||||
} else {
|
||||
log.Debug("sent %d bytes of json response", sent)
|
||||
}
|
||||
}
|
||||
|
||||
func ERROR(w http.ResponseWriter, statusCode int, err error) {
|
||||
if err != nil {
|
||||
JSON(w, statusCode, struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
JSON(w, http.StatusBadRequest, nil)
|
||||
}
|
||||
73
builder/arm_builder.sh
Executable file
73
builder/arm_builder.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
PROGRAM="${1}"
|
||||
shift
|
||||
COMMAND="${*}"
|
||||
|
||||
IMAGE="https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip"
|
||||
GOLANG="https://dl.google.com/go/go1.13.1.linux-armv6l.tar.gz"
|
||||
|
||||
REPO_DIR="${PWD}"
|
||||
TMP_DIR="/tmp/builder"
|
||||
MNT_DIR="${TMP_DIR}/mnt"
|
||||
|
||||
if ! systemctl is-active systemd-binfmt.service >/dev/null 2>&1; then
|
||||
mkdir -p "/lib/binfmt.d"
|
||||
echo ':qemu-arm:M::\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:F' > /lib/binfmt.d/qemu-arm-static.conf
|
||||
systemctl restart systemd-binfmt.service
|
||||
fi
|
||||
|
||||
mkdir -p "${TMP_DIR}"
|
||||
wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "${IMAGE}"
|
||||
gunzip -c "${TMP_DIR}/raspbian.zip" > "${TMP_DIR}/raspbian.img"
|
||||
truncate "${TMP_DIR}/raspbian.img" --size=+2G
|
||||
parted --script "${TMP_DIR}/raspbian.img" resizepart 2 100%
|
||||
|
||||
LOOP_PATH="$(losetup --find --partscan --show "${TMP_DIR}/raspbian.img")"
|
||||
e2fsck -y -f "${LOOP_PATH}p2"
|
||||
resize2fs "${LOOP_PATH}p2"
|
||||
partprobe "${LOOP_PATH}"
|
||||
|
||||
mkdir -p "${MNT_DIR}"
|
||||
mountpoint -q "${MNT_DIR}" && umount -R "${MNT_DIR}"
|
||||
mount -o rw "${LOOP_PATH}p2" "${MNT_DIR}"
|
||||
mount -o rw "${LOOP_PATH}p1" "${MNT_DIR}/boot"
|
||||
|
||||
mount --bind /dev "${MNT_DIR}/dev/"
|
||||
mount --bind /sys "${MNT_DIR}/sys/"
|
||||
mount --bind /proc "${MNT_DIR}/proc/"
|
||||
mount --bind /dev/pts "${MNT_DIR}/dev/pts"
|
||||
mount | grep "${MNT_DIR}"
|
||||
df -h
|
||||
|
||||
cp /usr/bin/qemu-arm-static "${MNT_DIR}/usr/bin"
|
||||
cp /etc/resolv.conf "${MNT_DIR}/etc/resolv.conf"
|
||||
|
||||
mkdir -p "${MNT_DIR}/root/src/${PROGRAM}"
|
||||
mount --bind "${REPO_DIR}" "${MNT_DIR}/root/src/${PROGRAM}"
|
||||
|
||||
cp "${MNT_DIR}/etc/ld.so.preload" "${MNT_DIR}/etc/_ld.so.preload"
|
||||
touch "${MNT_DIR}/etc/ld.so.preload"
|
||||
|
||||
chroot "${MNT_DIR}" bin/bash -x <<EOF
|
||||
set -eu
|
||||
|
||||
export LANG="C"
|
||||
export LC_ALL="C"
|
||||
export LC_CTYPE="C"
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/bin"
|
||||
|
||||
wget --show-progress -qcO /tmp/golang.tar.gz "${GOLANG}"
|
||||
tar -C /usr/local -xzf /tmp/golang.tar.gz
|
||||
export GOROOT="/usr/local/go"
|
||||
export GOPATH="/root"
|
||||
|
||||
apt-get -y update
|
||||
apt-get install wget libpcap-dev libusb-1.0-0-dev libnetfilter-queue-dev build-essential git
|
||||
|
||||
cd "/root/src/${PROGRAM}"
|
||||
${COMMAND}
|
||||
EOF
|
||||
echo "Build finished"
|
||||
57
changelog.sh
Executable file
57
changelog.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
NEW=()
|
||||
FIXES=()
|
||||
MISC=()
|
||||
|
||||
echo "@ Fetching remote tags ..."
|
||||
git fetch --tags >/dev/null
|
||||
printf "\n\n"
|
||||
|
||||
CURTAG=$(git describe --tags --abbrev=0)
|
||||
OUTPUT=$(git log $CURTAG..HEAD --oneline)
|
||||
IFS=$'\n' LINES=($OUTPUT)
|
||||
|
||||
for LINE in "${LINES[@]}"; do
|
||||
LINE=$(echo "$LINE" | sed -E "s/^[[:xdigit:]]+\s+//")
|
||||
if [[ $LINE == *"new:"* ]]; then
|
||||
LINE=$(echo "$LINE" | sed -E "s/^new: //")
|
||||
NEW+=("$LINE")
|
||||
elif [[ $LINE == *"fix:"* ]]; then
|
||||
LINE=$(echo "$LINE" | sed -E "s/^fix: //")
|
||||
FIXES+=("$LINE")
|
||||
elif [[ $LINE != *"i did not bother commenting"* ]] && [[ $LINE != *"Merge "* ]]; then
|
||||
echo " MISC LINE =$LINE"
|
||||
LINE=$(echo "$LINE" | sed -E "s/^[a-z]+: //")
|
||||
MISC+=("$LINE")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$NEW" ]; then
|
||||
echo
|
||||
echo "**New Features**"
|
||||
echo
|
||||
for l in "${NEW[@]}"; do
|
||||
echo "* $l"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$FIXES" ]; then
|
||||
echo
|
||||
echo "**Fixes**"
|
||||
echo
|
||||
for l in "${FIXES[@]}"; do
|
||||
echo "* $l"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$MISC" ]; then
|
||||
echo
|
||||
echo "**Misc**"
|
||||
echo
|
||||
for l in "${MISC[@]}"; do
|
||||
echo "* $l"
|
||||
done
|
||||
fi
|
||||
|
||||
echo
|
||||
180
cmd/shikigrid/inbox.go
Normal file
180
cmd/shikigrid/inbox.go
Normal file
@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
"github.com/andatoshiki/shikigrid/api"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func clearScreen() {
|
||||
var what []string
|
||||
if runtime.GOOS == "windows" {
|
||||
what = []string{"cmd", "/c", "cls"}
|
||||
} else {
|
||||
what = []string{"clear", ""}
|
||||
}
|
||||
cmd := exec.Command(what[0], what[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func showInbox(server *api.API, box map[string]interface{}) {
|
||||
messages := box["messages"].([]interface{})
|
||||
numMessages := len(messages)
|
||||
|
||||
if numMessages > 0 {
|
||||
if clear {
|
||||
log.Info("clearing %d messages", numMessages)
|
||||
for _, m := range messages {
|
||||
msg := m.(map[string]interface{})
|
||||
msgID := int(msg["id"].(float64))
|
||||
log.Info("deleting message %d ...", msgID)
|
||||
if _, err := server.Client.MarkInboxMessage(msgID, "deleted"); err != nil {
|
||||
log.Error("%v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
records := box["records"].(float64)
|
||||
pages := box["pages"].(float64)
|
||||
columns := []string{
|
||||
"ID",
|
||||
"Date",
|
||||
"Sender",
|
||||
}
|
||||
rows := [][]string{}
|
||||
for _, m := range messages {
|
||||
var row []string
|
||||
msg := m.(map[string]interface{})
|
||||
|
||||
t, err := time.Parse(time.RFC3339, msg["created_at"].(string))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
row = []string{
|
||||
fmt.Sprintf("%d", int(msg["id"].(float64))),
|
||||
t.Format("02 January 2006, 3:04 PM"),
|
||||
fmt.Sprintf("%s@%s", msg["sender_name"], msg["sender"]),
|
||||
}
|
||||
|
||||
if msg["seen_at"] != nil {
|
||||
for i := range row {
|
||||
row[i] = tui.Dim(row[i])
|
||||
}
|
||||
}
|
||||
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
tui.Table(os.Stdout, columns, rows)
|
||||
fmt.Println()
|
||||
|
||||
fmt.Printf("%d of %d (page %d of %d)", numMessages, int(records), page, int(pages))
|
||||
}
|
||||
} else {
|
||||
fmt.Println()
|
||||
fmt.Println(tui.Dim("Inbox is empty."))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func showMessage(msg map[string]interface{}) {
|
||||
t, err := time.Parse(time.RFC3339, msg["created_at"].(string))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("From: %s@%s\n", msg["sender_name"], msg["sender"])
|
||||
fmt.Printf("Date: %s\n\n", t.Format("02 January 2006, 3:04 PM"))
|
||||
if output == "" {
|
||||
fmt.Printf("%s\n", msg["data"])
|
||||
fmt.Println()
|
||||
} else if err := os.WriteFile(output, msg["data"].([]byte), os.ModePerm); err != nil {
|
||||
log.Fatal("error writing to %s: %v", output, err)
|
||||
} else {
|
||||
log.Info("%s written", output)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage() {
|
||||
var err error
|
||||
|
||||
// send a message
|
||||
var raw []byte
|
||||
if message == "" {
|
||||
log.Fatal("-message can not be empty")
|
||||
} else if message[0] == '@' {
|
||||
log.Info("reading %s ...", message[1:])
|
||||
if raw, err = os.ReadFile(message[1:]); err != nil {
|
||||
log.Fatal("error reading %s: %v", message[1:], err)
|
||||
}
|
||||
} else {
|
||||
raw = []byte(message)
|
||||
}
|
||||
|
||||
if status, err := server.SendMessage(receiver, raw); err != nil {
|
||||
log.Fatal("%d %v", status, err)
|
||||
} else {
|
||||
log.Info("message sent")
|
||||
}
|
||||
}
|
||||
|
||||
func doInbox(server *api.API) {
|
||||
if receiver != "" {
|
||||
sendMessage()
|
||||
} else if inbox {
|
||||
// just show the inbox
|
||||
if id == 0 {
|
||||
log.Info("fetching inbox ...")
|
||||
if box, err := server.Client.Inbox(page); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
} else {
|
||||
showInbox(server, box)
|
||||
}
|
||||
} else if del {
|
||||
log.Info("deleting message %d ...", id)
|
||||
if _, err := server.Client.MarkInboxMessage(id, "deleted"); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
} else if unread {
|
||||
log.Info("marking message %d as unread ...", id)
|
||||
if _, err := server.Client.MarkInboxMessage(id, "unseen"); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
} else {
|
||||
log.Info("fetching message %d ...", id)
|
||||
|
||||
if msg, status, err := server.InboxMessage(id); err != nil {
|
||||
log.Fatal("%d %v", status, err)
|
||||
} else {
|
||||
showMessage(msg)
|
||||
_, _ = server.Client.MarkInboxMessage(id, "seen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inboxMain() {
|
||||
if inbox {
|
||||
doInbox(server)
|
||||
if loop {
|
||||
ticker := time.NewTicker(time.Duration(loopPeriod) * time.Second)
|
||||
for _ = range ticker.C {
|
||||
clearScreen()
|
||||
doInbox(server)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
38
cmd/shikigrid/main.go
Normal file
38
cmd/shikigrid/main.go
Normal file
@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
setupCore()
|
||||
defer cleanup()
|
||||
|
||||
// just print the version and exit
|
||||
if ver {
|
||||
fmt.Println(version.Version)
|
||||
return
|
||||
}
|
||||
|
||||
// from here on we need logging
|
||||
if err := log.Open(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer log.Close()
|
||||
|
||||
// do mode related initialization
|
||||
setupMode()
|
||||
|
||||
// if we're in peer mode and is an inbox action
|
||||
if inbox {
|
||||
inboxMain()
|
||||
} else {
|
||||
// just start the API in either modes
|
||||
server.Run(address)
|
||||
}
|
||||
}
|
||||
190
cmd/shikigrid/setup.go
Normal file
190
cmd/shikigrid/setup.go
Normal file
@ -0,0 +1,190 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/api"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/mesh"
|
||||
"github.com/andatoshiki/shikigrid/models"
|
||||
"github.com/andatoshiki/shikigrid/utils"
|
||||
"github.com/andatoshiki/shikigrid/version"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func cleanup() {
|
||||
if cpuProfile != "" {
|
||||
log.Info("writing CPU profile to %s ...", cpuProfile)
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
if memProfile != "" {
|
||||
log.Info("writing memory profile to %s ...", memProfile)
|
||||
f, err := os.Create(memProfile)
|
||||
if err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupCore() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for sig := range c {
|
||||
log.Warning("received signal %v", sig)
|
||||
cleanup()
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
|
||||
if cpuProfile != "" {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Level = log.DEBUG
|
||||
} else {
|
||||
log.Level = log.INFO
|
||||
}
|
||||
log.OnFatal = log.ExitOnFatal
|
||||
}
|
||||
|
||||
func waitForKeys() {
|
||||
privPath := crypto.PrivatePath(keysPath)
|
||||
for {
|
||||
if !fs.Exists(privPath) {
|
||||
log.Debug("waiting for %s ...", privPath)
|
||||
time.Sleep(1 * time.Second)
|
||||
} else {
|
||||
// give it a moment to finish disk sync
|
||||
time.Sleep(2 * time.Second)
|
||||
log.Info("%s found", privPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupMesh() {
|
||||
var err error
|
||||
peer = mesh.MakeLocalPeer(utils.Hostname(), keys, advertise)
|
||||
if !advertise {
|
||||
return //this probably doesn't work
|
||||
}
|
||||
|
||||
if err = peer.StartAdvertising(iface); err != nil {
|
||||
log.Fatal("error while starting signaling: %v", err)
|
||||
}
|
||||
if router, err = mesh.StartRouting(iface, peersPath, peer); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
} else {
|
||||
router.OnNewPeer(func(ident string, peer *mesh.Peer) {
|
||||
log.Info("detected new peer %s on channel %d", peer.ID(), peer.Channel)
|
||||
})
|
||||
router.OnPeerLost(func(ident string, peer *mesh.Peer) {
|
||||
log.Info("peer %s lost (inactive for %fs)", peer.ID(), peer.InactiveFor())
|
||||
})
|
||||
}
|
||||
log.Info("peer %s signaling is ready", peer.ID())
|
||||
}
|
||||
|
||||
func setupDB() {
|
||||
if err := godotenv.Load(env); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
if err := models.Setup(); err != nil {
|
||||
if nodb {
|
||||
log.Warning("%v", err)
|
||||
} else {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setupMode() string {
|
||||
var err error
|
||||
|
||||
// in case -inbox was not explicitly passed
|
||||
if receiver != "" || loop == true || id > 0 {
|
||||
inbox = true
|
||||
}
|
||||
|
||||
// for inbox actions, set the keys to the default path if empty
|
||||
if (whoami || inbox) && keysPath == "" {
|
||||
keysPath = "/etc/shikigotchi/"
|
||||
}
|
||||
|
||||
// generate keypair
|
||||
if generate {
|
||||
if keysPath == "" {
|
||||
log.Fatal("no -keys path specified")
|
||||
} else if crypto.KeysExist(keysPath) {
|
||||
log.Fatal("keypair already exists in %s", keysPath)
|
||||
}
|
||||
|
||||
if _, err = crypto.LoadOrCreate(keysPath, 4096); err != nil {
|
||||
log.Fatal("error generating RSA keypair: %v", err)
|
||||
} else {
|
||||
log.Info("keypair saved to %s", keysPath)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
mode := "peer"
|
||||
// if keys have been passed explicitly, or one of the inbox actions
|
||||
// has been specified, we're running on the unit
|
||||
// if keysPath != "" {
|
||||
// mode = "peer"
|
||||
// }
|
||||
|
||||
log.Info("shikigrid v%s starting in %s mode ...", version.Version, mode)
|
||||
|
||||
// wait for keys to be generated
|
||||
if wait {
|
||||
waitForKeys()
|
||||
}
|
||||
// load the keys
|
||||
if keys, err = crypto.Load(keysPath); err != nil {
|
||||
log.Fatal("error while loading keys from %s: %v", keysPath, err)
|
||||
}
|
||||
// print identity and exit
|
||||
if whoami {
|
||||
if Endpoint == "https://grid-api.toshiki.dev/api/v1" {
|
||||
log.Info("https://grid.toshiki.dev/search/%s", keys.FingerprintHex)
|
||||
} else {
|
||||
log.Info("https://pwnagotchi.ai/pwnfile/#!%s", keys.FingerprintHex)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
// only start mesh signaling if this is not an inbox action
|
||||
if !inbox {
|
||||
setupMesh()
|
||||
}
|
||||
|
||||
// set up the proper routes for either server or peer mode
|
||||
err, server = api.Setup(keys, peer, router, Endpoint, Hostname)
|
||||
if err != nil {
|
||||
log.Fatal("%v", err)
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
83
cmd/shikigrid/vars.go
Normal file
83
cmd/shikigrid/vars.go
Normal file
@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/andatoshiki/shikigrid/api"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/mesh"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = false
|
||||
ver = false
|
||||
wait = false
|
||||
inbox = false
|
||||
del = false
|
||||
unread = false
|
||||
clear = false
|
||||
whoami = false
|
||||
generate = false
|
||||
loop = false
|
||||
nodb = false
|
||||
loopPeriod = 30
|
||||
receiver = ""
|
||||
message = ""
|
||||
output = ""
|
||||
page = 1
|
||||
id = 0
|
||||
address = "0.0.0.0:8666"
|
||||
env = ".env"
|
||||
iface = "wlan0mon"
|
||||
keysPath = ""
|
||||
peersPath = "/root/peers"
|
||||
keys = (*crypto.KeyPair)(nil)
|
||||
router = (*mesh.Router)(nil)
|
||||
peer = (*mesh.Peer)(nil)
|
||||
server = (*api.API)(nil)
|
||||
cpuProfile = ""
|
||||
memProfile = ""
|
||||
Endpoint = "https://grid-api.toshiki.dev/api/v1"
|
||||
advertise = true
|
||||
Hostname = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&ver, "version", ver, "Print version and exit.")
|
||||
flag.BoolVar(&debug, "debug", debug, "Enable debug logs.")
|
||||
flag.BoolVar(&nodb, "no-db", debug, "Don't fail if database connection can't be enstablished.")
|
||||
flag.StringVar(&log.Output, "log", log.Output, "Log file path or empty for standard output.")
|
||||
flag.StringVar(&address, "address", address, "API address.")
|
||||
flag.StringVar(&env, "env", env, "Load .env from.")
|
||||
|
||||
flag.StringVar(&keysPath, "keys", keysPath, "If set, will load RSA keys from this folder and start in peer mode.")
|
||||
flag.BoolVar(&generate, "generate", generate, "Generate an RSA keypair if it doesn't exist yet.")
|
||||
flag.BoolVar(&wait, "wait", wait, "Wait for keys to be generated.")
|
||||
flag.IntVar(&api.ClientTimeout, "client-timeout", api.ClientTimeout, "Timeout in seconds for requests to the server when in peer mode.")
|
||||
flag.StringVar(&api.ClientTokenFile, "client-token", api.ClientTokenFile, "File where to store the API token.")
|
||||
|
||||
flag.StringVar(&iface, "iface", iface, "Monitor interface to use for mesh advertising.")
|
||||
flag.StringVar(&peersPath, "peers", peersPath, "path to save historical information of met peers.")
|
||||
flag.IntVar(&mesh.SignalingPeriod, "signaling-period", mesh.SignalingPeriod, "Period in milliseconds for mesh signaling frames.")
|
||||
|
||||
flag.BoolVar(&whoami, "whoami", whoami, "Prints the public key fingerprint and exit.")
|
||||
flag.BoolVar(&inbox, "inbox", inbox, "Show inbox.")
|
||||
flag.BoolVar(&loop, "loop", loop, "Keep refreshing and showing inbox.")
|
||||
flag.IntVar(&loopPeriod, "loop-period", loopPeriod, "Period in seconds to refresh the inbox.")
|
||||
flag.StringVar(&receiver, "send", receiver, "Receiver unit fingerprint.")
|
||||
flag.StringVar(&message, "message", message, "Message body or file path if prefixed by @.")
|
||||
flag.StringVar(&output, "output", output, "Write message body to this file instead of the standard output.")
|
||||
flag.BoolVar(&del, "delete", del, "Delete the specified message.")
|
||||
flag.BoolVar(&unread, "unread", unread, "Unread the specified message.")
|
||||
flag.BoolVar(&clear, "clear", unread, "Delete all messages of the given page of the inbox.")
|
||||
flag.IntVar(&page, "page", page, "Inbox page.")
|
||||
flag.IntVar(&id, "id", id, "Message id.")
|
||||
|
||||
flag.StringVar(&cpuProfile, "cpu-profile", cpuProfile, "Generate CPU profile to this file.")
|
||||
flag.StringVar(&memProfile, "mem-profile", cpuProfile, "Generate memory profile to this file.")
|
||||
|
||||
flag.StringVar(&Endpoint, "endpoint", Endpoint, "Pass which endpoint shikigrid should be using.")
|
||||
flag.BoolVar(&advertise, "advertise", advertise, "Advertise?")
|
||||
flag.StringVar(&Hostname, "hostname", Hostname, "Pass hostname to shikigrid, makes it so it wont read os.hostname()")
|
||||
}
|
||||
120
crypto/encrypt.go
Normal file
120
crypto/encrypt.go
Normal file
@ -0,0 +1,120 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
AESKEyLength = 32
|
||||
NonceLength = 12
|
||||
)
|
||||
|
||||
func (pair *KeyPair) EncryptFor(cleartext []byte, pubKey *rsa.PublicKey) ([]byte, error) {
|
||||
// generate a random 32 bytes long key
|
||||
key := make([]byte, AESKEyLength)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// encrypt the key with RSA
|
||||
encKey, err := pair.EncryptBlockFor(key, pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use that key to encrypt the cleartext in AES-GCM
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nonce := make([]byte, NonceLength)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encrypted := gcm.Seal(nil, nonce, cleartext, nil)
|
||||
|
||||
keySizeBuf := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(keySizeBuf, uint32(len(encKey)))
|
||||
|
||||
// send all
|
||||
encrypted = append(encKey, encrypted...) // key enc
|
||||
encrypted = append(keySizeBuf, encrypted...) // ksz key enc
|
||||
encrypted = append(nonce, encrypted...) // nonce ksz key enc
|
||||
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
func (pair *KeyPair) EncryptBlockFor(block []byte, pubKey *rsa.PublicKey) ([]byte, error) {
|
||||
return rsa.EncryptOAEP(
|
||||
Hasher.New(),
|
||||
rand.Reader,
|
||||
pubKey,
|
||||
block,
|
||||
[]byte(""))
|
||||
}
|
||||
|
||||
func (pair *KeyPair) DecryptBlock(block []byte) ([]byte, error) {
|
||||
return rsa.DecryptOAEP(
|
||||
Hasher.New(),
|
||||
rand.Reader,
|
||||
pair.Private,
|
||||
block,
|
||||
[]byte(""))
|
||||
}
|
||||
|
||||
func (pair *KeyPair) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
dataAvailable := len(ciphertext)
|
||||
if dataAvailable < NonceLength {
|
||||
return nil, fmt.Errorf("data buffer too short")
|
||||
}
|
||||
|
||||
nonce := ciphertext[0:NonceLength]
|
||||
dataAvailable -= NonceLength
|
||||
|
||||
if dataAvailable < 4 {
|
||||
return nil, fmt.Errorf("data buffer too short")
|
||||
}
|
||||
|
||||
keySizeBuf := ciphertext[NonceLength : NonceLength+4]
|
||||
keySize := binary.LittleEndian.Uint32(keySizeBuf)
|
||||
dataAvailable -= 4
|
||||
|
||||
if dataAvailable < int(keySize) {
|
||||
return nil, fmt.Errorf("data buffer too short")
|
||||
}
|
||||
|
||||
encKey := ciphertext[NonceLength+4 : NonceLength+4+keySize]
|
||||
ciphertext = ciphertext[NonceLength+4+keySize:]
|
||||
|
||||
// decrypt the key
|
||||
key, err := pair.DecryptBlock(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// decrypt the payload
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
173
crypto/keypair.go
Normal file
173
crypto/keypair.go
Normal file
@ -0,0 +1,173 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
Path string
|
||||
Bits int
|
||||
PrivatePath string
|
||||
Private *rsa.PrivateKey
|
||||
PrivatePEM []byte
|
||||
PublicPath string
|
||||
Public *rsa.PublicKey
|
||||
PublicPEM []byte
|
||||
// sha256 of PublicSSH
|
||||
Fingerprint []byte
|
||||
FingerprintHex string
|
||||
}
|
||||
|
||||
func pubKeyToPEM(key *rsa.PublicKey) ([]byte, error) {
|
||||
bytes, err := x509.MarshalPKIXPublicKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PUBLIC KEY",
|
||||
Bytes: bytes,
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func FromPublicPEM(pubPEM string) (pair *KeyPair, err error) {
|
||||
block, _ := pem.Decode([]byte(pubPEM))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to parse PEM block containing the public key")
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pair = &KeyPair{}
|
||||
ok := false
|
||||
|
||||
if pair.Public, ok = pub.(*rsa.PublicKey); !ok {
|
||||
return nil, fmt.Errorf("not an RSA key")
|
||||
}
|
||||
|
||||
return pair, pair.setupPublic()
|
||||
}
|
||||
|
||||
func PrivatePath(keysPath string) string {
|
||||
return path.Join(keysPath, "id_rsa")
|
||||
}
|
||||
|
||||
func Load(keysPath string) (pair *KeyPair, err error) {
|
||||
privFile := PrivatePath(keysPath)
|
||||
pair = &KeyPair{
|
||||
Path: keysPath,
|
||||
PrivatePath: privFile,
|
||||
PublicPath: privFile + ".pub",
|
||||
}
|
||||
return pair, pair.Load()
|
||||
}
|
||||
|
||||
func KeysExist(keysPath string) bool {
|
||||
return fs.Exists(keysPath) && fs.Exists(PrivatePath(keysPath))
|
||||
}
|
||||
|
||||
func LoadOrCreate(keysPath string, bits int) (pair *KeyPair, err error) {
|
||||
privFile := PrivatePath(keysPath)
|
||||
pair = &KeyPair{
|
||||
Path: keysPath,
|
||||
Bits: bits,
|
||||
PrivatePath: privFile,
|
||||
PublicPath: privFile + ".pub",
|
||||
}
|
||||
|
||||
if !fs.Exists(pair.PrivatePath) {
|
||||
if !fs.Exists(keysPath) {
|
||||
log.Debug("creating %s", keysPath)
|
||||
if err := os.MkdirAll(keysPath, os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("could not create %s: %v", keysPath, err)
|
||||
}
|
||||
}
|
||||
log.Info("%s not found, generating keypair ...", pair.PrivatePath)
|
||||
|
||||
if pair.Private, err = rsa.GenerateKey(rand.Reader, bits); err != nil {
|
||||
return nil, fmt.Errorf("could not generate private key: %v", err)
|
||||
}
|
||||
pair.Public = &pair.Private.PublicKey
|
||||
|
||||
if err = pair.Save(); err != nil {
|
||||
return nil, fmt.Errorf("could not save keypair: %v", err)
|
||||
}
|
||||
} else if err = pair.Load(); err != nil {
|
||||
return nil, fmt.Errorf("could not load keypair: %v", err)
|
||||
}
|
||||
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
func (pair *KeyPair) setupPublic() (err error) {
|
||||
if pair.PublicPEM, err = pubKeyToPEM(pair.Public); err != nil {
|
||||
return fmt.Errorf("failed converting public key to PEM: %v", err)
|
||||
}
|
||||
|
||||
cleanPEM := strings.TrimRight(string(pair.PublicPEM), "\n")
|
||||
|
||||
hash := Hasher.New()
|
||||
hash.Write([]byte(cleanPEM))
|
||||
|
||||
pair.Fingerprint = hash.Sum(nil)
|
||||
pair.FingerprintHex = fmt.Sprintf("%02x", pair.Fingerprint)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pair *KeyPair) Save() (err error) {
|
||||
prvKeyBytes := x509.MarshalPKCS1PrivateKey(pair.Private)
|
||||
pair.PrivatePEM = pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: prvKeyBytes,
|
||||
},
|
||||
)
|
||||
|
||||
if err = os.WriteFile(pair.PrivatePath, pair.PrivatePEM, os.ModePerm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("%s created", pair.PrivatePath)
|
||||
|
||||
if err = pair.setupPublic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(pair.PublicPath, pair.PublicPEM, os.ModePerm)
|
||||
|
||||
log.Debug("%s created", pair.PublicPath)
|
||||
return
|
||||
}
|
||||
|
||||
func (pair *KeyPair) Load() (err error) {
|
||||
log.Debug("reading %s ...", pair.PrivatePath)
|
||||
if pair.PrivatePEM, err = os.ReadFile(pair.PrivatePath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(pair.PrivatePEM)
|
||||
if block == nil {
|
||||
return fmt.Errorf("failed decoding PEM from %s", pair.PrivatePath)
|
||||
}
|
||||
|
||||
if pair.Private, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
|
||||
return fmt.Errorf("failed parsing %s: %v", pair.PrivatePath, err)
|
||||
}
|
||||
|
||||
pair.Public = &pair.Private.PublicKey
|
||||
return pair.setupPublic()
|
||||
}
|
||||
42
crypto/sign.go
Normal file
42
crypto/sign.go
Normal file
@ -0,0 +1,42 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
)
|
||||
|
||||
var pssOpts = rsa.PSSOptions{
|
||||
SaltLength: 16,
|
||||
}
|
||||
|
||||
const Hasher = crypto.SHA256
|
||||
|
||||
func (pair *KeyPair) Sign(hash crypto.Hash, hashed []byte) ([]byte, error) {
|
||||
return rsa.SignPSS(rand.Reader, pair.Private, hash, hashed, &pssOpts)
|
||||
}
|
||||
|
||||
func (pair *KeyPair) SignMessage(data []byte) ([]byte, error) {
|
||||
hasher := Hasher.New()
|
||||
hasher.Write(data)
|
||||
hash := hasher.Sum(nil)
|
||||
return pair.Sign(Hasher, hash)
|
||||
}
|
||||
|
||||
func (pair *KeyPair) Verify(signature []byte, hasher crypto.Hash, hash []byte) error {
|
||||
return rsa.VerifyPSS(
|
||||
pair.Public,
|
||||
hasher,
|
||||
hash,
|
||||
signature,
|
||||
&pssOpts)
|
||||
}
|
||||
|
||||
func (pair *KeyPair) VerifyMessage(data []byte, signature []byte) error {
|
||||
hasher := Hasher.New()
|
||||
hasher.Write(data)
|
||||
hash := hasher.Sum(nil)
|
||||
// log.Info("hash(data) = %x", hash)
|
||||
// log.Info("signature = %x", signature)
|
||||
return pair.Verify(signature, Hasher, hash)
|
||||
}
|
||||
54
docker-compose.yml
Normal file
54
docker-compose.yml
Normal file
@ -0,0 +1,54 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
container_name: shikigrid_api
|
||||
build: .
|
||||
ports:
|
||||
- 8666:8666
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- api:/usr/src/app/
|
||||
depends_on:
|
||||
- shikigrid-mysql
|
||||
networks:
|
||||
- shikigrid
|
||||
|
||||
shikigrid-mysql:
|
||||
image: mysql:5.7
|
||||
container_name: shikigrid_mysql
|
||||
ports:
|
||||
- 3306:3306
|
||||
environment:
|
||||
- MYSQL_ROOT_HOST=${DB_HOST}
|
||||
- MYSQL_USER=${DB_USER}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_DATABASE=${DB_NAME}
|
||||
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
|
||||
volumes:
|
||||
- database_mysql:/var/lib/mysql
|
||||
networks:
|
||||
- shikigrid
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin
|
||||
container_name: shikigrid_phpmyadmin
|
||||
depends_on:
|
||||
- shikigrid-mysql
|
||||
environment:
|
||||
- PMA_HOST=shikigrid-mysql
|
||||
- PMA_USER=${DB_USER}
|
||||
- PMA_PORT=${DB_PORT}
|
||||
- PMA_PASSWORD=${DB_PASSWORD}
|
||||
ports:
|
||||
- 9090:80
|
||||
restart: always
|
||||
networks:
|
||||
- shikigrid
|
||||
|
||||
volumes:
|
||||
api:
|
||||
database_mysql:
|
||||
|
||||
networks:
|
||||
shikigrid:
|
||||
driver: bridge
|
||||
8
env.example
Normal file
8
env.example
Normal file
@ -0,0 +1,8 @@
|
||||
API_SECRET=02zygnJs5e0bBLJjaHCinWTjfRdheTYO
|
||||
|
||||
DB_HOST=shikigrid-mysql
|
||||
DB_DRIVER=mysql
|
||||
DB_USER=shikigrid
|
||||
DB_PASSWORD=shikigrid
|
||||
DB_NAME=shikigrid
|
||||
DB_PORT=3306
|
||||
21
go.mod
Normal file
21
go.mod
Normal file
@ -0,0 +1,21 @@
|
||||
module github.com/andatoshiki/shikigrid
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/biezhi/gorm-paginator/pagination v0.0.0-20190124091837-7a5c8ed20334
|
||||
github.com/evilsocket/islazy v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/gopacket/gopacket v1.2.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
)
|
||||
58
go.sum
Normal file
58
go.sum
Normal file
@ -0,0 +1,58 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/biezhi/gorm-paginator/pagination v0.0.0-20190124091837-7a5c8ed20334 h1:ptFjQ4+vPGZDiNmBuKUetQoREFiPz/WB29CfQfdfeKc=
|
||||
github.com/biezhi/gorm-paginator/pagination v0.0.0-20190124091837-7a5c8ed20334/go.mod h1:Y/N4aF7p+Med/9ivVSsGBc8xOs8BGptUVBCY3k4KFCY=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
|
||||
github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/gopacket/gopacket v1.2.0 h1:eXbzFad7f73P1n2EJHQlsKuvIMJjVXK5tXoSca78I3A=
|
||||
github.com/gopacket/gopacket v1.2.0/go.mod h1:BrAKEy5EOGQ76LSqh7DMAr7z0NNPdczWm2GxCG7+I8M=
|
||||
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
|
||||
github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
|
||||
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
|
||||
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
41
mesh/hopping.go
Normal file
41
mesh/hopping.go
Normal file
@ -0,0 +1,41 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ChannelHopping(iface string, chanList string, allChannels []int, hopPeriod int) {
|
||||
var channels []int
|
||||
for _, s := range str.Comma(chanList) {
|
||||
if ch, err := strconv.Atoi(s); err != nil {
|
||||
log.Fatal("%v", err)
|
||||
} else {
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
}
|
||||
if len(channels) == 0 {
|
||||
channels = allChannels
|
||||
}
|
||||
sort.Ints(channels)
|
||||
|
||||
go func() {
|
||||
period := time.Duration(hopPeriod) * time.Millisecond
|
||||
tick := time.NewTicker(period)
|
||||
|
||||
log.Info("channel hopper started (period:%s channels:%v)", period, channels)
|
||||
|
||||
loop := 0
|
||||
for _ = range tick.C {
|
||||
ch := channels[loop%len(channels)]
|
||||
// log.Debug("hopping on channel %d", ch)
|
||||
if err, out := SetChannel(iface, ch); err != nil {
|
||||
log.Error("%v: %s", err, out)
|
||||
}
|
||||
loop++
|
||||
}
|
||||
}()
|
||||
}
|
||||
51
mesh/interface.go
Normal file
51
mesh/interface.go
Normal file
@ -0,0 +1,51 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/andatoshiki/shikigrid/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var chanParser = regexp.MustCompile(`^\s+Channel.([0-9]+)\s+:\s+([0-9.]+)\s+GHz.*$`)
|
||||
|
||||
func ActivateInterface(name string) error {
|
||||
if out, err := utils.Exec("ifconfig", []string{name, "up"}); err != nil {
|
||||
return err
|
||||
} else if out != "" {
|
||||
return fmt.Errorf("unexpected output while activating interface %s: %s", name, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetChannel(iface string, channel int) (error, string) {
|
||||
if out, err := utils.Exec("iwconfig", []string{iface, "channel", fmt.Sprintf("%d", channel)}); err != nil {
|
||||
return err, out
|
||||
} else if out != "" {
|
||||
return fmt.Errorf("unexpected output while setting interface %s to channel %d: %s", iface, channel, out), out
|
||||
} else {
|
||||
return nil, out
|
||||
}
|
||||
}
|
||||
|
||||
func SupportedChannels(iface string) ([]int, error) {
|
||||
out, err := utils.Exec("iwlist", []string{iface, "freq"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []int
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if matches := chanParser.FindStringSubmatch(line); len(matches) == 3 {
|
||||
if channel, err := strconv.ParseInt(matches[1], 10, 32); err == nil {
|
||||
channels = append(channels, int(channel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
122
mesh/memory.go
Normal file
122
mesh/memory.go
Normal file
@ -0,0 +1,122 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Memory struct {
|
||||
sync.Mutex
|
||||
path string
|
||||
peers map[string]*Peer
|
||||
}
|
||||
|
||||
func MemoryFromPath(path string) (err error, mem *Memory) {
|
||||
if path, err = fs.Expand(path); err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
mem = &Memory{
|
||||
path: path,
|
||||
peers: make(map[string]*Peer),
|
||||
}
|
||||
|
||||
if !fs.Exists(path) {
|
||||
log.Debug("creating %s ...", path)
|
||||
if err = os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = fs.Glob(path, "*.json", func(fileName string) error {
|
||||
log.Debug("loading %s ...", fileName)
|
||||
data, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
log.Error("error loading %s: %v", fileName, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var peer jsonPeer
|
||||
if err = json.Unmarshal(data, &peer); err != nil {
|
||||
log.Error("error loading %s: %v", fileName, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
mem.peers[peer.Fingerprint] = peerFromJSON(peer)
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Debug("loaded %d known peers", len(mem.peers))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (mem *Memory) Size() int {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
return len(mem.peers)
|
||||
}
|
||||
|
||||
func (mem *Memory) Of(fingerprint string) *Peer {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
|
||||
if peer, found := mem.peers[fingerprint]; found {
|
||||
return peer
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mem *Memory) List() []*Peer {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
|
||||
list := make([]*Peer, 0)
|
||||
for _, peer := range mem.peers {
|
||||
list = append(list, peer)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (mem *Memory) Track(fingerprint string, peer *Peer) error {
|
||||
mem.Lock()
|
||||
defer mem.Unlock()
|
||||
|
||||
if encounter, found := mem.peers[fingerprint]; !found {
|
||||
// peer first encounter
|
||||
peer.Encounters = 1
|
||||
peer.MetAt = time.Now()
|
||||
peer.PrevSeenAt = peer.SeenAt
|
||||
} else {
|
||||
// we met this peer before
|
||||
if encounter.Encounters < math.MaxUint64 {
|
||||
encounter.Encounters++
|
||||
}
|
||||
peer.PrevSeenAt = encounter.SeenAt
|
||||
peer.MetAt = encounter.MetAt
|
||||
peer.Encounters = encounter.Encounters
|
||||
}
|
||||
|
||||
peer.SeenAt = time.Now()
|
||||
|
||||
// save/update peer data in memory
|
||||
mem.peers[fingerprint] = peer
|
||||
// save/update peer data on disk
|
||||
fileName := path.Join(mem.path, fmt.Sprintf("%s.json", fingerprint))
|
||||
if data, err := json.Marshal(peer); err != nil {
|
||||
return err
|
||||
} else if err := os.WriteFile(fileName, data, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
144
mesh/packet_muxer.go
Normal file
144
mesh/packet_muxer.go
Normal file
@ -0,0 +1,144 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/async"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/pcap"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrIfaceNotUp Ugly, but gopacket folks are not exporting pcap errors, so ...
|
||||
// ref. https://github.com/gopacket/gopacket/blob/96986c90e3e5c7e01deed713ff8058e357c0c047/pcap/pcap.go#L281
|
||||
ErrIfaceNotUp = "Interface Not Up"
|
||||
)
|
||||
|
||||
var (
|
||||
SnapLength = 65536
|
||||
ReadTimeout = 100
|
||||
)
|
||||
|
||||
type PacketCallback func(pkt gopacket.Packet)
|
||||
|
||||
type PacketMuxer struct {
|
||||
iface string
|
||||
filter string
|
||||
handle *pcap.Handle
|
||||
source *gopacket.PacketSource
|
||||
channel chan gopacket.Packet
|
||||
queue *async.WorkQueue
|
||||
stop chan struct{}
|
||||
|
||||
onPacket PacketCallback
|
||||
}
|
||||
|
||||
func dummyPacketCallback(pkt gopacket.Packet) {
|
||||
|
||||
}
|
||||
|
||||
func NewPacketMuxer(iface, filter string, workers int) (mux *PacketMuxer, err error) {
|
||||
mux = &PacketMuxer{
|
||||
iface: iface,
|
||||
filter: filter,
|
||||
stop: make(chan struct{}),
|
||||
onPacket: dummyPacketCallback,
|
||||
}
|
||||
|
||||
for retry := 0; ; retry++ {
|
||||
inactiveHandle, err := pcap.NewInactiveHandle(iface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while opening interface %s: %s", iface, err)
|
||||
}
|
||||
defer inactiveHandle.CleanUp()
|
||||
|
||||
if err = inactiveHandle.SetRFMon(true); err != nil {
|
||||
log.Warning("error while setting interface %s in monitor mode: %s", iface, err)
|
||||
}
|
||||
|
||||
if err = inactiveHandle.SetSnapLen(SnapLength); err != nil {
|
||||
return nil, fmt.Errorf("error while settng span len: %s", err)
|
||||
}
|
||||
/*
|
||||
* We don't want to pcap.BlockForever otherwise pcap_close(handle)
|
||||
* could hang waiting for a timeout to expire ...
|
||||
*/
|
||||
readTimeout := time.Duration(ReadTimeout) * time.Millisecond
|
||||
if err = inactiveHandle.SetTimeout(readTimeout); err != nil {
|
||||
return nil, fmt.Errorf("error while setting timeout: %s", err)
|
||||
} else if mux.handle, err = inactiveHandle.Activate(); err != nil {
|
||||
if retry == 0 && err.Error() == ErrIfaceNotUp {
|
||||
log.Info("interface %s is down, bringing it up ...", iface)
|
||||
if err := ActivateInterface(iface); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("error while activating handle: %s", err)
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
if err := mux.handle.SetBPFFilter(filter); err != nil {
|
||||
return nil, fmt.Errorf("error setting BPF filter '%s': %v", filter, err)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
mux.source = gopacket.NewPacketSource(mux.handle, mux.handle.LinkType())
|
||||
mux.channel = mux.source.Packets()
|
||||
mux.queue = async.NewQueue(workers, func(arg async.Job) {
|
||||
mux.onPacket(arg.(gopacket.Packet))
|
||||
})
|
||||
|
||||
return mux, nil
|
||||
}
|
||||
|
||||
func (mux *PacketMuxer) OnPacket(cb PacketCallback) {
|
||||
mux.onPacket = cb
|
||||
}
|
||||
|
||||
func (mux *PacketMuxer) Write(data []byte) error {
|
||||
var err error
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
if err = mux.handle.WritePacketData(data); err == nil {
|
||||
return nil
|
||||
} else if strings.Contains(err.Error(), "temporarily unavailable") {
|
||||
log.Debug("resource temporarily unavailable when sending data")
|
||||
// if it's the last attempt this will set err to nil as we can't really
|
||||
// do a lot about this case, otherwise it'll wait 200ms before the next
|
||||
// attempt is made.
|
||||
err = nil
|
||||
if attempt < 5 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (mux *PacketMuxer) Start() {
|
||||
go func() {
|
||||
log.Debug("packet muxer started (iface:%s filter:%s)", mux.iface, mux.filter)
|
||||
for {
|
||||
select {
|
||||
case packet := <-mux.channel:
|
||||
mux.queue.Add(async.Job(packet))
|
||||
case <-mux.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (mux *PacketMuxer) Stop() {
|
||||
log.Debug("stopping packet muxer ...")
|
||||
mux.stop <- struct{}{}
|
||||
mux.queue.WaitDone()
|
||||
log.Debug("packet muxer stopped")
|
||||
}
|
||||
386
mesh/peer.go
Normal file
386
mesh/peer.go
Normal file
@ -0,0 +1,386 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"github.com/andatoshiki/shikigrid/version"
|
||||
"github.com/andatoshiki/shikigrid/wifi"
|
||||
)
|
||||
|
||||
var (
|
||||
SignalingPeriod = 300
|
||||
|
||||
fingValidator = regexp.MustCompile("^[a-fA-F0-9]{64}$")
|
||||
)
|
||||
|
||||
type SessionID []byte
|
||||
|
||||
type Peer struct {
|
||||
sync.Mutex
|
||||
|
||||
MetAt time.Time // first time met
|
||||
DetectedAt time.Time // first time detected on this session
|
||||
SeenAt time.Time // last time detected on this session
|
||||
PrevSeenAt time.Time // if we met this unit before, this is the last time it's been seen
|
||||
Encounters uint64
|
||||
Channel int
|
||||
RSSI int
|
||||
SessionID SessionID
|
||||
SessionIDStr string
|
||||
Keys *crypto.KeyPair
|
||||
AdvData sync.Map
|
||||
AdvPeriod int
|
||||
|
||||
advEnabled bool
|
||||
ForceDisabled bool
|
||||
|
||||
mux *PacketMuxer
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func MakeLocalPeer(name string, keys *crypto.KeyPair, advertise bool) *Peer {
|
||||
now := time.Now()
|
||||
peer := &Peer{
|
||||
DetectedAt: now,
|
||||
SeenAt: now,
|
||||
PrevSeenAt: now,
|
||||
SessionID: make([]byte, 6),
|
||||
Keys: keys,
|
||||
AdvData: sync.Map{},
|
||||
AdvPeriod: SignalingPeriod,
|
||||
stop: make(chan struct{}),
|
||||
advEnabled: false,
|
||||
ForceDisabled: false,
|
||||
}
|
||||
if !advertise {
|
||||
peer.ForceDisabled = true
|
||||
}
|
||||
|
||||
if _, err := rand.Read(peer.SessionID); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
parts := make([]string, 6)
|
||||
for idx, byte := range peer.SessionID {
|
||||
parts[idx] = fmt.Sprintf("%02x", byte)
|
||||
}
|
||||
peer.SessionIDStr = strings.Join(parts, ":")
|
||||
|
||||
peer.AdvData.Store("name", name)
|
||||
peer.AdvData.Store("identity", keys.FingerprintHex)
|
||||
peer.AdvData.Store("session_id", peer.SessionIDStr)
|
||||
peer.AdvData.Store("grid_version", version.Version)
|
||||
|
||||
peer.AdvData.Range(func(key, value interface{}) bool {
|
||||
log.Debug("local.adv.%s = %s", key, value)
|
||||
return true
|
||||
})
|
||||
|
||||
return peer
|
||||
}
|
||||
|
||||
func (peer *Peer) Advertise(enabled bool) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
diff := peer.advEnabled != enabled
|
||||
peer.advEnabled = enabled
|
||||
if diff {
|
||||
if enabled {
|
||||
log.Info("peer advertisement enabled")
|
||||
} else {
|
||||
log.Info("peer advertisement disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewPeer(radiotap *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (peer *Peer, err error) {
|
||||
now := time.Now()
|
||||
peer = &Peer{
|
||||
DetectedAt: now,
|
||||
SeenAt: now,
|
||||
PrevSeenAt: now,
|
||||
Channel: wifi.Freq2Chan(int(radiotap.ChannelFrequency)),
|
||||
RSSI: int(radiotap.DBMAntennaSignal),
|
||||
SessionID: SessionID(dot11.Address3),
|
||||
AdvData: sync.Map{},
|
||||
}
|
||||
|
||||
parts := make([]string, 6)
|
||||
for idx, byte := range peer.SessionID {
|
||||
parts[idx] = fmt.Sprintf("%02x", byte)
|
||||
}
|
||||
peer.SessionIDStr = strings.Join(parts, ":")
|
||||
|
||||
// parse the fingerprint, the signature and the public key
|
||||
fingerprint, found := adv["identity"].(string)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
|
||||
} else if !fingValidator.MatchString(fingerprint) {
|
||||
return nil, fmt.Errorf("peer %x is advertising an invalid fingerprint: %s", peer.SessionID, fingerprint)
|
||||
}
|
||||
|
||||
if pubKey64, found := adv["public_key"]; found {
|
||||
pubKey, err := base64.StdEncoding.DecodeString(pubKey64.(string))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding peer %s public key: %s", fingerprint, err)
|
||||
}
|
||||
|
||||
peer.Keys, err = crypto.FromPublicPEM(string(pubKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing peer %s public key: %s", fingerprint, err)
|
||||
}
|
||||
|
||||
// basic consistency check
|
||||
if peer.Keys.FingerprintHex != fingerprint {
|
||||
return nil, fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
|
||||
}
|
||||
} else if !found {
|
||||
log.Debug("peer %s is not advertising any public key", fingerprint)
|
||||
}
|
||||
|
||||
/*
|
||||
No need for signature in the advertisement protocol, however:
|
||||
|
||||
signature64, found := adv["signature"].(string)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("peer %s is advertising unsigned data", fingerprint)
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(signature64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding peer %s signature: %s", fingerprint, err)
|
||||
}
|
||||
|
||||
// the signature is SIGN(advertisement), so we need to remove the signature field and convert back to json.
|
||||
// NOTE: fortunately, keys will be always sorted, so we don't have to do anything in order to guarantee signature
|
||||
// consistency (https://stackoverflow.com/questions/18668652/how-to-produce-json-with-sorted-keys-in-go)
|
||||
signedMap := adv
|
||||
delete(signedMap, "signature")
|
||||
|
||||
signedData, err := json.Marshal(signedMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error packing data for signature verification: %v", err)
|
||||
}
|
||||
|
||||
// verify the signature
|
||||
if err = peer.Keys.VerifyMessage(signedData, signature); err != nil {
|
||||
return nil, fmt.Errorf("peer %x signature is invalid", peer.SessionID)
|
||||
}
|
||||
*/
|
||||
|
||||
for key, value := range adv {
|
||||
peer.AdvData.Store(key, value)
|
||||
}
|
||||
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (peer *Peer) Update(radio *layers.RadioTap, dot11 *layers.Dot11, adv map[string]interface{}) (err error) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
|
||||
// parse the fingerprint, the signature and the public key
|
||||
fingerprint, found := adv["identity"].(string)
|
||||
if !found {
|
||||
return fmt.Errorf("peer %x is not advertising any identity", peer.SessionID)
|
||||
}
|
||||
|
||||
// basic consistency check
|
||||
if peer.Keys != nil && peer.Keys.FingerprintHex != fingerprint {
|
||||
return fmt.Errorf("peer %x is advertising fingerprint %s, but it should be %s", peer.SessionID, fingerprint, peer.Keys.FingerprintHex)
|
||||
}
|
||||
|
||||
/*
|
||||
No need for signature in the advertisement protocol, however:
|
||||
|
||||
signature64, found := adv["signature"].(string)
|
||||
if !found {
|
||||
return fmt.Errorf("peer %x is not advertising any signature", peer.SessionID)
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(signature64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding peer %d signature: %s", peer.SessionID, err)
|
||||
}
|
||||
|
||||
// the signature is SIGN(advertisement), so we need to remove the signature field and convert back to json.
|
||||
// NOTE: fortunately, keys will always be sorted, so we don't have to do anything in order to guarantee signature
|
||||
// consistency (https://stackoverflow.com/questions/18668652/how-to-produce-json-with-sorted-keys-in-go)
|
||||
signedMap := adv
|
||||
delete(signedMap, "signature")
|
||||
|
||||
signedData, err := json.Marshal(signedMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error packing data for signature verification: %v", err)
|
||||
}
|
||||
|
||||
// verify the signature
|
||||
if err = peer.Keys.VerifyMessage(signedData, signature); err != nil {
|
||||
return fmt.Errorf("peer %x signature is invalid", peer.SessionID)
|
||||
}
|
||||
*/
|
||||
|
||||
peer.Channel = wifi.Freq2Chan(int(radio.ChannelFrequency))
|
||||
peer.RSSI = int(radio.DBMAntennaSignal)
|
||||
|
||||
if !bytes.Equal(peer.SessionID, dot11.Address3) {
|
||||
log.Info("peer %s changed session id: %x -> %x", peer.ID(), peer.SessionIDStr, dot11.Address3)
|
||||
copy(peer.SessionID, dot11.Address3)
|
||||
parts := make([]string, 6)
|
||||
for idx, byte := range peer.SessionID {
|
||||
parts[idx] = fmt.Sprintf("%02x", byte)
|
||||
}
|
||||
peer.SessionIDStr = strings.Join(parts, ":")
|
||||
}
|
||||
|
||||
for key, value := range adv {
|
||||
peer.AdvData.Store(key, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *Peer) ID() string {
|
||||
name, _ := peer.AdvData.Load("name")
|
||||
ident := "???"
|
||||
|
||||
if peer.Keys != nil {
|
||||
ident = peer.Keys.FingerprintHex
|
||||
} else if _ident, found := peer.AdvData.Load("identity"); found {
|
||||
ident = _ident.(string)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s@%s", name, ident)
|
||||
}
|
||||
|
||||
func (peer *Peer) InactiveFor() float64 {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
return time.Since(peer.DetectedAt).Seconds()
|
||||
}
|
||||
|
||||
func (peer *Peer) SetData(adv map[string]interface{}) {
|
||||
if peer == nil {
|
||||
return
|
||||
}
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
|
||||
for key, val := range adv {
|
||||
if val == nil {
|
||||
peer.AdvData.Delete(key)
|
||||
} else {
|
||||
peer.AdvData.Store(key, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *Peer) Data() map[string]interface{} {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
return peer.dataFrame()
|
||||
}
|
||||
|
||||
func (peer *Peer) dataFrame() map[string]interface{} {
|
||||
data := map[string]interface{}{}
|
||||
peer.AdvData.Range(func(key, value interface{}) bool {
|
||||
data[key.(string)] = value
|
||||
return true
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
func (peer *Peer) advertise() {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
|
||||
if peer.advEnabled {
|
||||
data := peer.dataFrame()
|
||||
|
||||
data["timestamp"] = time.Now().Unix()
|
||||
adv, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Error("could not serialize advertisement data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
No need for signature in the advertisement protocol, however:
|
||||
|
||||
// sign the advertisement
|
||||
signature, err := peer.Keys.SignMessage(adv)
|
||||
if err != nil {
|
||||
log.Error("error signing advertisement: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// add the signature to the advertisement itself and encode again
|
||||
data["signature"] = base64.StdEncoding.EncodeToString(signature)
|
||||
adv, err = json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Error("could not serialize signed advertisement data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("advertising:\n%+v", data)
|
||||
*/
|
||||
|
||||
err, raw := wifi.Pack(
|
||||
net.HardwareAddr(peer.SessionID),
|
||||
wifi.BroadcastAddr,
|
||||
adv,
|
||||
false) // set compression to true if using signature
|
||||
if err != nil {
|
||||
log.Error("could not encapsulate %d bytes of advertisement data: %v", len(adv), err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = peer.mux.Write(raw); err != nil {
|
||||
log.Error("error sending %d bytes of advertisement frame: %v", len(raw), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *Peer) StartAdvertising(iface string) (err error) {
|
||||
if peer.mux == nil {
|
||||
if peer.mux, err = NewPacketMuxer(iface, "", Workers); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
period := time.Duration(peer.AdvPeriod) * time.Millisecond
|
||||
ticker := time.NewTicker(period)
|
||||
|
||||
log.Debug("advertiser started with a %s period", period)
|
||||
|
||||
for {
|
||||
select {
|
||||
case _ = <-ticker.C:
|
||||
peer.advertise()
|
||||
case <-peer.stop:
|
||||
log.Info("advertiser stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *Peer) StopAdvertising() {
|
||||
log.Debug("stopping advertiser ...")
|
||||
peer.stop <- struct{}{}
|
||||
}
|
||||
81
mesh/peer_json.go
Normal file
81
mesh/peer_json.go
Normal file
@ -0,0 +1,81 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type jsonPeer struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
MetAt time.Time `json:"met_at"`
|
||||
DetectedAt time.Time `json:"detected_at"`
|
||||
SeenAt time.Time `json:"seen_at"`
|
||||
PrevSeenAt time.Time `json:"prev_seen_at"`
|
||||
Encounters uint64 `json:"encounters"`
|
||||
Channel int `json:"channel"`
|
||||
RSSI int `json:"rssi"`
|
||||
SessionID string `json:"session_id"`
|
||||
Advertisement map[string]interface{} `json:"advertisement"`
|
||||
}
|
||||
|
||||
// creates a Peer object filled with the fields of the JSON representation
|
||||
func peerFromJSON(j jsonPeer) *Peer {
|
||||
peer := &Peer{
|
||||
DetectedAt: j.DetectedAt,
|
||||
SeenAt: j.SeenAt,
|
||||
PrevSeenAt: j.PrevSeenAt,
|
||||
SessionIDStr: j.SessionID,
|
||||
Encounters: j.Encounters,
|
||||
Channel: j.Channel,
|
||||
RSSI: j.RSSI,
|
||||
AdvData: sync.Map{},
|
||||
}
|
||||
|
||||
if hw, err := net.ParseMAC(j.SessionID); err == nil {
|
||||
copy(peer.SessionID, hw)
|
||||
} else {
|
||||
log.Warning("error parsing peer session id %s: %v", j.SessionID, err)
|
||||
}
|
||||
|
||||
for key, val := range j.Advertisement {
|
||||
peer.AdvData.Store(key, val)
|
||||
}
|
||||
|
||||
return peer
|
||||
}
|
||||
|
||||
// converts a peer into a JSON friendly representation
|
||||
func (peer *Peer) json() *jsonPeer {
|
||||
fingerprint := ""
|
||||
if v, found := peer.AdvData.Load("identity"); found {
|
||||
fingerprint = v.(string)
|
||||
}
|
||||
|
||||
doc := jsonPeer{
|
||||
Fingerprint: fingerprint,
|
||||
MetAt: peer.MetAt,
|
||||
Encounters: peer.Encounters,
|
||||
PrevSeenAt: peer.PrevSeenAt,
|
||||
DetectedAt: peer.DetectedAt,
|
||||
SeenAt: peer.SeenAt,
|
||||
Channel: peer.Channel,
|
||||
RSSI: peer.RSSI,
|
||||
SessionID: peer.SessionIDStr,
|
||||
Advertisement: make(map[string]interface{}),
|
||||
}
|
||||
peer.AdvData.Range(func(key, value interface{}) bool {
|
||||
doc.Advertisement[key.(string)] = value
|
||||
return true
|
||||
})
|
||||
|
||||
return &doc
|
||||
}
|
||||
|
||||
func (peer *Peer) MarshalJSON() ([]byte, error) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
return json.Marshal(peer.json())
|
||||
}
|
||||
163
mesh/routing.go
Normal file
163
mesh/routing.go
Normal file
@ -0,0 +1,163 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
"github.com/andatoshiki/shikigrid/wifi"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Workers = 0
|
||||
PeerTTL = 1800
|
||||
Peers = sync.Map{}
|
||||
)
|
||||
|
||||
func dummyPeerActivityCallback(ident string, peer *Peer) {}
|
||||
|
||||
type PeerActivityCallback func(ident string, peer *Peer)
|
||||
|
||||
type Router struct {
|
||||
local *Peer
|
||||
mux *PacketMuxer
|
||||
onNewPeer PeerActivityCallback
|
||||
onPeerLost PeerActivityCallback
|
||||
memory *Memory
|
||||
}
|
||||
|
||||
func StartRouting(iface string, peersPath string, local *Peer) (*Router, error) {
|
||||
err, memory := MemoryFromPath(peersPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filter := fmt.Sprintf("type mgt subtype beacon and ether src %s", wifi.SignatureAddrStr)
|
||||
mux, err := NewPacketMuxer(iface, filter, Workers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
router := &Router{
|
||||
mux: mux,
|
||||
local: local,
|
||||
memory: memory,
|
||||
onNewPeer: dummyPeerActivityCallback,
|
||||
onPeerLost: dummyPeerActivityCallback,
|
||||
}
|
||||
mux.OnPacket(router.onPacket)
|
||||
mux.Start()
|
||||
|
||||
log.Info("started beacon discovery and message routing (%d known peers)", router.memory.Size())
|
||||
|
||||
go router.peersPruner()
|
||||
|
||||
return router, nil
|
||||
}
|
||||
|
||||
func (router *Router) Memory() []*Peer {
|
||||
return router.memory.List()
|
||||
}
|
||||
|
||||
func (router *Router) MemoryOf(fingerprint string) *Peer {
|
||||
return router.memory.Of(fingerprint)
|
||||
}
|
||||
|
||||
func (router *Router) OnNewPeer(cb PeerActivityCallback) {
|
||||
router.onNewPeer = cb
|
||||
}
|
||||
|
||||
func (router *Router) OnPeerLost(cb PeerActivityCallback) {
|
||||
router.onPeerLost = cb
|
||||
}
|
||||
|
||||
func (router *Router) peersPruner() {
|
||||
period := time.Duration(500) * time.Millisecond
|
||||
tick := time.NewTicker(period)
|
||||
|
||||
log.Debug("peers pruner started with a %s period", period)
|
||||
|
||||
for range tick.C {
|
||||
stale := map[string]*Peer{}
|
||||
|
||||
Peers.Range(func(key, value interface{}) bool {
|
||||
ident := key.(string)
|
||||
peer := value.(*Peer)
|
||||
inactive := peer.InactiveFor()
|
||||
if int(inactive) > PeerTTL {
|
||||
stale[ident] = peer
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
for ident, peer := range stale {
|
||||
Peers.Delete(ident)
|
||||
router.onPeerLost(ident, peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (router *Router) newPeer(ident string, peer *Peer) {
|
||||
Peers.Store(ident, peer)
|
||||
router.onNewPeer(ident, peer)
|
||||
}
|
||||
|
||||
func (router *Router) onPeerAdvertisement(pkt gopacket.Packet, radio *layers.RadioTap, dot11 *layers.Dot11) {
|
||||
err, payload := wifi.Unpack(pkt, radio, dot11)
|
||||
if err != nil {
|
||||
log.Debug("%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
advData := make(map[string]interface{})
|
||||
if err := json.Unmarshal(payload, &advData); err != nil {
|
||||
log.Debug("error decoding payload '%s': %v", payload, err)
|
||||
return
|
||||
}
|
||||
|
||||
ident, ok := advData["identity"]
|
||||
if !ok {
|
||||
log.Debug("error parsing identity from payload '%s'", payload)
|
||||
return
|
||||
}
|
||||
|
||||
var peer *Peer
|
||||
|
||||
_peer, existing := Peers.Load(ident)
|
||||
if existing {
|
||||
peer = _peer.(*Peer)
|
||||
if err := peer.Update(radio, dot11, advData); err != nil {
|
||||
log.Warning("error updating peer %s: %v", peer.ID(), err)
|
||||
} else if err := router.memory.Track(ident.(string), peer); err != nil {
|
||||
log.Error("error saving peer encounter for %s: %v", ident, err)
|
||||
}
|
||||
} else {
|
||||
if peer, err = NewPeer(radio, dot11, advData); err != nil {
|
||||
log.Debug("error creating peer: %v", err)
|
||||
return
|
||||
} else if err := router.memory.Track(ident.(string), peer); err != nil {
|
||||
log.Error("error saving peer encounter for %s: %v", ident, err)
|
||||
} else {
|
||||
router.newPeer(ident.(string), peer)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (router *Router) onPacket(pkt gopacket.Packet) {
|
||||
if ok, radio, dot11 := wifi.Parse(pkt); ok && dot11.ChecksumValid() {
|
||||
src := dot11.Address3
|
||||
dst := dot11.Address1
|
||||
if !bytes.Equal(src, router.local.SessionID) {
|
||||
if bytes.Equal(dst, wifi.BroadcastAddr) {
|
||||
router.onPeerAdvertisement(pkt, radio, dot11)
|
||||
} else {
|
||||
// log.Debug("ignoring message %x > %x", src, dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
models/access_point.go
Normal file
11
models/access_point.go
Normal file
@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
import "github.com/jinzhu/gorm"
|
||||
|
||||
type AccessPoint struct {
|
||||
gorm.Model
|
||||
|
||||
UnitID uint `json:"-"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Mac string `gorm:"size:255;not null" json:"mac"`
|
||||
}
|
||||
74
models/enrollment.go
Normal file
74
models/enrollment.go
Normal file
@ -0,0 +1,74 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"github.com/andatoshiki/shikigrid/crypto"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EnrollmentRequest struct {
|
||||
Identity string `json:"identity"` // name@SHA256(public_key)
|
||||
PublicKey string `json:"public_key"` // BASE64(public_key.pem)
|
||||
Signature string `json:"signature"` // BASE64(SIGN(identity, private_key))
|
||||
Data map[string]interface{} `json:"data"` // misc data for the unit
|
||||
KeyPair *crypto.KeyPair `json:"-"` // parsed from public_key
|
||||
Name string `json:"-"`
|
||||
Fingerprint string `json:"-"` // SHA256(public_key)
|
||||
Address string `json:"-"`
|
||||
Country string `json:"-"`
|
||||
}
|
||||
|
||||
var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
|
||||
|
||||
func clean(s string) string {
|
||||
for _, m := range ansi.FindAllString(s, -1) {
|
||||
s = strings.Replace(s, m, "", -1)
|
||||
}
|
||||
return str.Trim(s)
|
||||
}
|
||||
|
||||
func (enroll *EnrollmentRequest) Validate() error {
|
||||
// split the identity into name and fingerprint
|
||||
parts := strings.Split(enroll.Identity, "@")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("error parsing the identity string: got %d parts", len(parts))
|
||||
}
|
||||
|
||||
enroll.Name = clean(parts[0])
|
||||
enroll.Fingerprint = clean(parts[1])
|
||||
if len(enroll.Fingerprint) != crypto.Hasher.Size()*2 {
|
||||
return fmt.Errorf("unexpected fingerprint length for %s", enroll.Fingerprint)
|
||||
}
|
||||
|
||||
// parse the public key as b64 pem
|
||||
pubKeyPEM, err := base64.StdEncoding.DecodeString(enroll.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding the public key: %v", err)
|
||||
}
|
||||
|
||||
enroll.KeyPair, err = crypto.FromPublicPEM(string(pubKeyPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing the public key: %v", err)
|
||||
}
|
||||
|
||||
enroll.PublicKey = string(enroll.KeyPair.PublicPEM)
|
||||
|
||||
if enroll.KeyPair.FingerprintHex != enroll.Fingerprint {
|
||||
return fmt.Errorf("fingerprint mismatch: expected:%s got:%s", enroll.KeyPair.FingerprintHex, enroll.Fingerprint)
|
||||
}
|
||||
|
||||
data := []byte(enroll.Identity)
|
||||
signature, err := base64.StdEncoding.DecodeString(enroll.Signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding the signature: %v", err)
|
||||
}
|
||||
|
||||
if err := enroll.KeyPair.VerifyMessage(data, signature); err != nil {
|
||||
return fmt.Errorf("signature verification failed: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
models/message.go
Normal file
35
models/message.go
Normal file
@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MessageDataMaxSize = 512000
|
||||
MessageSignatureMaxSize = 10000
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
ID uint `gorm:"primary_key" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `sql:"index" json:"deleted_at"`
|
||||
SeenAt *time.Time `json:"seen_at" sql:"index"`
|
||||
SenderID uint `json:"-"`
|
||||
ReceiverID uint `json:"-"`
|
||||
SenderName string `gorm:"size:255" json:"sender_name"`
|
||||
Sender string `gorm:"size:255;not null" json:"sender"`
|
||||
Data string `gorm:"size:512000;not null" json:"-"`
|
||||
Signature string `gorm:"size:10000;not null" json:"-"`
|
||||
}
|
||||
|
||||
func ValidateMessage(data, signature string) error {
|
||||
// validate max sizes
|
||||
if dataSize := len(data); dataSize > MessageDataMaxSize {
|
||||
return fmt.Errorf("max message data size is %d", MessageDataMaxSize)
|
||||
} else if sigSize := len(signature); sigSize > MessageSignatureMaxSize {
|
||||
return fmt.Errorf("max message signature size is %d", MessageSignatureMaxSize)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
38
models/setup.go
Normal file
38
models/setup.go
Normal file
@ -0,0 +1,38 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/jinzhu/gorm"
|
||||
"os"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func Setup() (err error) {
|
||||
hostname := os.Getenv("DB_HOST")
|
||||
port := os.Getenv("DB_PORT")
|
||||
username := os.Getenv("DB_USER")
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
name := os.Getenv("DB_NAME")
|
||||
|
||||
log.Info("connecting to %s:%s ...", hostname, port)
|
||||
dbURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, hostname, port, name)
|
||||
if db, err = gorm.Open("mysql", dbURL); err != nil {
|
||||
return
|
||||
}
|
||||
db.Debug().AutoMigrate(&Unit{}, &AccessPoint{}, &Message{})
|
||||
return
|
||||
}
|
||||
|
||||
func Create(v interface{}) *gorm.DB {
|
||||
return db.Create(v)
|
||||
}
|
||||
|
||||
func Update(v interface{}) *gorm.DB {
|
||||
return db.Model(v).Update(v)
|
||||
}
|
||||
|
||||
func UpdateFields(v interface{}, fields map[string]interface{}) *gorm.DB {
|
||||
return db.Model(v).Updates(fields)
|
||||
}
|
||||
181
models/unit.go
Normal file
181
models/unit.go
Normal file
@ -0,0 +1,181 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenTTL = time.Minute * 30
|
||||
CacheTTL = time.Minute * 120
|
||||
)
|
||||
|
||||
type cachedCounter struct {
|
||||
Time time.Time
|
||||
Count int
|
||||
}
|
||||
|
||||
var (
|
||||
cache = make(map[uint]*cachedCounter)
|
||||
cacheLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
type Unit struct {
|
||||
ID uint `gorm:"primary_key" json:"-"`
|
||||
CreatedAt time.Time `json:"enrolled_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `sql:"index" json:"-"`
|
||||
Address string `gorm:"size:50;not null" json:"-"`
|
||||
Country string `gorm:"size:10" json:"country"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Fingerprint string `gorm:"size:255;not null;unique" json:"fingerprint"`
|
||||
PublicKey string `gorm:"size:10000;not null" json:"public_key"`
|
||||
Token string `gorm:"size:10000;not null" json:"-"`
|
||||
Data string `gorm:"size:10000;not null" json:"data"`
|
||||
|
||||
AccessPoints []AccessPoint `gorm:"foreignkey:UnitID" json:"-"`
|
||||
|
||||
Inbox []Message `gorm:"foreignkey:ReceiverID" json:"-"`
|
||||
Sent []Message `gorm:"foreignkey:SenderID" json:"-"`
|
||||
}
|
||||
|
||||
func (u Unit) Identity() string {
|
||||
return fmt.Sprintf("%s@%s", u.Name, u.Fingerprint)
|
||||
}
|
||||
|
||||
func (u Unit) FindAccessPoint(essid, bssid string) *AccessPoint {
|
||||
var ap AccessPoint
|
||||
|
||||
if err := db.Where("unit_id = ? AND name = ? AND mac = ?", u.ID, essid, bssid).Take(&ap).Error; err != nil {
|
||||
if err := db.Where("unit_id = ? AND mac = ?", u.ID, bssid).Take(&ap).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return &ap
|
||||
}
|
||||
|
||||
func (u *Unit) updateToken() error {
|
||||
claims := jwt.MapClaims{}
|
||||
claims["authorized"] = true
|
||||
claims["unit_id"] = u.ID
|
||||
claims["unit_ident"] = u.Identity()
|
||||
claims["expires_at"] = time.Now().Add(TokenTTL).Format(time.RFC3339)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := token.SignedString([]byte(os.Getenv("API_SECRET")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Token = signed
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Unit) UpdateWith(enroll EnrollmentRequest) error {
|
||||
prevData := map[string]interface{}{}
|
||||
|
||||
if u.Data != "" {
|
||||
if err := json.Unmarshal([]byte(u.Data), &prevData); err != nil {
|
||||
log.Warning("error parsing previous data: %v", err)
|
||||
log.Debug("%s", u.Data)
|
||||
}
|
||||
}
|
||||
|
||||
// only replace sent values
|
||||
for key, obj := range enroll.Data {
|
||||
set := true
|
||||
if key == "session" {
|
||||
if session, ok := obj.(map[string]interface{}); !ok {
|
||||
set = false
|
||||
log.Warning("corrupted session (first level): %v", obj)
|
||||
} else if epochs, found := session["epochs"]; !found {
|
||||
set = false
|
||||
log.Warning("corrupted session (no epochs): %v", obj)
|
||||
} else if num, ok := epochs.(float64); !ok {
|
||||
set = false
|
||||
log.Warning("corrupted session (epochs type %v): %v", reflect.TypeOf(epochs), obj)
|
||||
} else if num == 0 {
|
||||
// do not update with empty sessions
|
||||
set = false
|
||||
}
|
||||
}
|
||||
|
||||
if set {
|
||||
prevData[key] = obj
|
||||
}
|
||||
}
|
||||
|
||||
newData, err := json.Marshal(prevData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Name != enroll.Name {
|
||||
log.Info("unit %s changed name: %s -> %s", u.Identity(), u.Name, enroll.Name)
|
||||
}
|
||||
|
||||
u.Name = enroll.Name
|
||||
u.Address = enroll.Address
|
||||
u.Country = enroll.Country
|
||||
u.Data = string(newData)
|
||||
|
||||
return db.Save(u).Error
|
||||
}
|
||||
|
||||
type unitJSON struct {
|
||||
EnrolledAt time.Time `json:"enrolled_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Country string `json:"country"`
|
||||
Name string `json:"name"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Networks int `json:"networks"`
|
||||
}
|
||||
|
||||
func (u *Unit) apCounter() int {
|
||||
cacheLock.Lock()
|
||||
defer cacheLock.Unlock()
|
||||
|
||||
count := -1
|
||||
if cnt, found := cache[u.ID]; found && time.Since(cnt.Time) < CacheTTL {
|
||||
count = cnt.Count
|
||||
}
|
||||
|
||||
if count == -1 {
|
||||
count = db.Model(u).Association("AccessPoints").Count()
|
||||
cache[u.ID] = &cachedCounter{
|
||||
Time: time.Now(),
|
||||
Count: count,
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (u *Unit) MarshalJSON() ([]byte, error) {
|
||||
doc := unitJSON{
|
||||
EnrolledAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
Country: u.Country,
|
||||
Name: u.Name,
|
||||
Fingerprint: u.Fingerprint,
|
||||
PublicKey: u.PublicKey,
|
||||
Data: map[string]interface{}{},
|
||||
Networks: u.apCounter(),
|
||||
}
|
||||
|
||||
if u.Data != "" {
|
||||
if err := json.Unmarshal([]byte(u.Data), &doc.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(doc)
|
||||
}
|
||||
34
models/unit_enroll.go
Normal file
34
models/unit_enroll.go
Normal file
@ -0,0 +1,34 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
)
|
||||
|
||||
func EnrollUnit(enroll EnrollmentRequest) (err error, unit *Unit) {
|
||||
if unit = FindUnitByFingerprint(enroll.Fingerprint); unit == nil {
|
||||
log.Info("enrolling new unit for %s (%s): %s", enroll.Address, enroll.Country, enroll.Identity)
|
||||
|
||||
unit = &Unit{
|
||||
Address: enroll.Address,
|
||||
Country: enroll.Country,
|
||||
Name: enroll.Name,
|
||||
Fingerprint: enroll.Fingerprint,
|
||||
PublicKey: string(enroll.KeyPair.PublicPEM),
|
||||
}
|
||||
|
||||
if err := db.Create(unit).Error; err != nil {
|
||||
return fmt.Errorf("error enrolling %s: %v", unit.Identity(), err), nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := unit.updateToken(); err != nil {
|
||||
return fmt.Errorf("error creating token for %s: %v", unit.Identity(), err), nil
|
||||
}
|
||||
|
||||
if err = unit.UpdateWith(enroll); err != nil {
|
||||
log.Debug("%+v", enroll)
|
||||
return fmt.Errorf("error setting token for %s: %v", unit.Identity(), err), nil
|
||||
}
|
||||
return nil, unit
|
||||
}
|
||||
44
models/unit_find.go
Normal file
44
models/unit_find.go
Normal file
@ -0,0 +1,44 @@
|
||||
package models
|
||||
|
||||
import "github.com/biezhi/gorm-paginator/pagination"
|
||||
|
||||
type UnitsByCountry struct {
|
||||
Country string `json:"country"`
|
||||
Count int `json:"units"`
|
||||
}
|
||||
|
||||
func GetUnitsByCountry() ([]UnitsByCountry, error) {
|
||||
results := make([]UnitsByCountry, 0)
|
||||
if err := db.Raw("SELECT country,COUNT(id) AS count FROM units GROUP BY country ORDER BY count DESC").Scan(&results).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func GetPagedUnits(page int) (units []Unit, total int, pages int) {
|
||||
paginator := pagination.Paging(&pagination.Param{
|
||||
DB: db,
|
||||
Page: page,
|
||||
Limit: 25,
|
||||
OrderBy: []string{"id desc"},
|
||||
}, &units)
|
||||
return units, paginator.TotalRecord, paginator.TotalPage
|
||||
}
|
||||
|
||||
func FindUnit(id uint) *Unit {
|
||||
var unit Unit
|
||||
if err := db.Find(&unit, id).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &unit
|
||||
}
|
||||
|
||||
func FindUnitByFingerprint(fingerprint string) *Unit {
|
||||
var unit Unit
|
||||
if fingerprint == "" {
|
||||
return nil
|
||||
} else if err := db.Where("fingerprint = ?", fingerprint).Take(&unit).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &unit
|
||||
}
|
||||
22
models/unit_inbox.go
Normal file
22
models/unit_inbox.go
Normal file
@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "github.com/biezhi/gorm-paginator/pagination"
|
||||
|
||||
func (u *Unit) GetPagedInbox(page int) (messages []Message, total int, pages int) {
|
||||
query := db.Model(Message{}).Where("receiver_id = ?", u.ID)
|
||||
paginator := pagination.Paging(&pagination.Param{
|
||||
DB: query,
|
||||
Page: page,
|
||||
Limit: 50,
|
||||
OrderBy: []string{"id desc"},
|
||||
}, &messages)
|
||||
return messages, paginator.TotalRecord, paginator.TotalPage
|
||||
}
|
||||
|
||||
func (u *Unit) GetInboxMessage(id int) *Message {
|
||||
var msg Message
|
||||
if err := db.Where("receiver_id = ? AND id = ?", u.ID, id).First(&msg).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
return &msg
|
||||
}
|
||||
31
release.sh
Executable file
31
release.sh
Executable file
@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# nothing to see here, just a utility i use to create new releases ^_^
|
||||
|
||||
VERSION_FILE=$(dirname "${BASH_SOURCE[0]}")/version/ver.go
|
||||
echo "version file is $VERSION_FILE"
|
||||
CURRENT_VERSION=$(cat "$VERSION_FILE" | grep Version | cut -d '"' -f 2)
|
||||
TO_UPDATE=(
|
||||
# shellcheck disable=SC2206
|
||||
$VERSION_FILE
|
||||
)
|
||||
|
||||
echo -n "current version is $CURRENT_VERSION, select new version: "
|
||||
read NEW_VERSION
|
||||
# shellcheck disable=SC2028
|
||||
echo "creating version $NEW_VERSION ...\n"
|
||||
|
||||
for file in "${TO_UPDATE[@]}"; do
|
||||
echo "patching $file ..."
|
||||
sed -i.bak "s/$CURRENT_VERSION/$NEW_VERSION/g" "$file"
|
||||
rm -rf "$file.bak"
|
||||
git add "$file"
|
||||
done
|
||||
|
||||
git commit -m "releasing v$NEW_VERSION"
|
||||
git push
|
||||
git tag -a v"$NEW_VERSION" -m "release v$NEW_VERSION"
|
||||
# shellcheck disable=SC2086
|
||||
git push origin v$NEW_VERSION
|
||||
|
||||
echo
|
||||
echo "All done, v$NEW_VERSION released ^_^"
|
||||
14
shikigrid-peer.service
Normal file
14
shikigrid-peer.service
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=shikigrid peer service
|
||||
Documentation=https://shikigotchi.org/
|
||||
Wants=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/shikigrid -log /var/log/shikigrid.log -peers /root/peers -address 127.0.0.1:8666
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
14
shikigrid.service
Normal file
14
shikigrid.service
Normal file
@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=shikigrid api service
|
||||
Documentation=https://shikigotchi.org/
|
||||
Wants=network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/shikigrid -log /var/log/shikigrid.log -env /etc/shikigrid/shikigrid.conf -address 127.0.0.1:8666
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
20
utils/exec.go
Normal file
20
utils/exec.go
Normal file
@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Exec(executable string, args []string) (string, error) {
|
||||
path, err := exec.LookPath(executable)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
raw, err := exec.Command(path, args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
return str.Trim(string(raw)), nil
|
||||
}
|
||||
}
|
||||
21
utils/host.go
Normal file
21
utils/host.go
Normal file
@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Hostname() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Warning("%v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.HasSuffix(name, ".local") {
|
||||
name = strings.Replace(name, ".local", "", -1)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
5
version/ver.go
Normal file
5
version/ver.go
Normal file
@ -0,0 +1,5 @@
|
||||
package version
|
||||
|
||||
const (
|
||||
Version = "0.0.1"
|
||||
)
|
||||
44
wifi/compression.go
Normal file
44
wifi/compression.go
Normal file
@ -0,0 +1,44 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func Compress(data []byte) (bool, []byte, error) {
|
||||
oldSize := len(data)
|
||||
buf := bytes.Buffer{}
|
||||
if zw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression); err != nil {
|
||||
return false, nil, fmt.Errorf("error initializing payload compression: %v", err)
|
||||
} else if _, err := zw.Write(data); err != nil {
|
||||
return false, nil, fmt.Errorf("error during payload compression: %v", err)
|
||||
} else if err = zw.Close(); err != nil {
|
||||
return false, nil, fmt.Errorf("error while finalizing payload compression: %v", err)
|
||||
}
|
||||
|
||||
compressed := buf.Bytes()
|
||||
newSize := len(compressed)
|
||||
|
||||
// log.Debug("gzip: %d > %d", oldSize, newSize)
|
||||
|
||||
if newSize < oldSize {
|
||||
return true, compressed, nil
|
||||
}
|
||||
return false, data, nil
|
||||
}
|
||||
|
||||
func Decompress(data []byte) ([]byte, error) {
|
||||
if zr, err := gzip.NewReader(bytes.NewBuffer(data)); err != nil {
|
||||
return nil, fmt.Errorf("error initializing payload decompression: %v", err)
|
||||
} else {
|
||||
defer func(zr *gzip.Reader) {
|
||||
err := zr.Close()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
}(zr)
|
||||
return io.ReadAll(zr)
|
||||
}
|
||||
}
|
||||
27
wifi/defines.go
Normal file
27
wifi/defines.go
Normal file
@ -0,0 +1,27 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
IDWhisperPayload layers.Dot11InformationElementID = 222
|
||||
IDWhisperCompression layers.Dot11InformationElementID = 223
|
||||
IDWhisperIdentity layers.Dot11InformationElementID = 224
|
||||
IDWhisperSignature layers.Dot11InformationElementID = 225
|
||||
IDWhisperStreamHeader layers.Dot11InformationElementID = 226
|
||||
)
|
||||
|
||||
var SerializationOptions = gopacket.SerializeOptions{
|
||||
FixLengths: true,
|
||||
ComputeChecksums: true,
|
||||
}
|
||||
|
||||
var (
|
||||
SignatureAddr = net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}
|
||||
SignatureAddrStr = "de:ad:be:ef:de:ad"
|
||||
BroadcastAddr = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
wpaFlags = 1041
|
||||
)
|
||||
86
wifi/pack.go
Normal file
86
wifi/pack.go
Normal file
@ -0,0 +1,86 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
"net"
|
||||
)
|
||||
|
||||
func Info(id layers.Dot11InformationElementID, info []byte) *layers.Dot11InformationElement {
|
||||
return &layers.Dot11InformationElement{
|
||||
ID: id,
|
||||
Length: uint8(len(info) & 0xff),
|
||||
Info: info,
|
||||
}
|
||||
}
|
||||
|
||||
func PackOneOf(from, to net.HardwareAddr, peerID []byte, signature []byte, streamID uint64, seqNum uint64, seqTot uint64, payload []byte, compress bool) (error, []byte) {
|
||||
stack := []gopacket.SerializableLayer{
|
||||
&layers.RadioTap{},
|
||||
&layers.Dot11{
|
||||
Address1: to,
|
||||
Address2: SignatureAddr,
|
||||
Address3: from,
|
||||
Type: layers.Dot11TypeMgmtBeacon,
|
||||
},
|
||||
&layers.Dot11MgmtBeacon{
|
||||
Flags: uint16(wpaFlags),
|
||||
Interval: 100,
|
||||
},
|
||||
}
|
||||
|
||||
if peerID != nil {
|
||||
stack = append(stack, Info(IDWhisperIdentity, peerID))
|
||||
}
|
||||
|
||||
if signature != nil {
|
||||
stack = append(stack, Info(IDWhisperSignature, signature))
|
||||
}
|
||||
|
||||
if streamID > 0 {
|
||||
streamBuf := new(bytes.Buffer)
|
||||
if err := binary.Write(streamBuf, binary.LittleEndian, streamID); err != nil {
|
||||
return err, nil
|
||||
} else if err = binary.Write(streamBuf, binary.LittleEndian, seqNum); err != nil {
|
||||
return err, nil
|
||||
} else if err = binary.Write(streamBuf, binary.LittleEndian, seqTot); err != nil {
|
||||
return err, nil
|
||||
}
|
||||
stack = append(stack, Info(IDWhisperStreamHeader, streamBuf.Bytes()))
|
||||
}
|
||||
|
||||
if compress {
|
||||
if didCompress, compressed, err := Compress(payload); err != nil {
|
||||
return err, nil
|
||||
} else if didCompress {
|
||||
stack = append(stack, Info(IDWhisperCompression, []byte{1}))
|
||||
payload = compressed
|
||||
}
|
||||
}
|
||||
|
||||
dataSize := len(payload)
|
||||
dataLeft := dataSize
|
||||
dataOff := 0
|
||||
chunkSize := 0xff
|
||||
|
||||
for dataLeft > 0 {
|
||||
sz := chunkSize
|
||||
if dataLeft < chunkSize {
|
||||
sz = dataLeft
|
||||
}
|
||||
|
||||
chunk := payload[dataOff : dataOff+sz]
|
||||
stack = append(stack, Info(IDWhisperPayload, chunk))
|
||||
|
||||
dataOff += sz
|
||||
dataLeft -= sz
|
||||
}
|
||||
|
||||
return Serialize(stack...)
|
||||
}
|
||||
|
||||
func Pack(from, to net.HardwareAddr, payload []byte, compress bool) (error, []byte) {
|
||||
return PackOneOf(from, to, nil, nil, 0, 0, 0, payload, compress)
|
||||
}
|
||||
30
wifi/parse.go
Normal file
30
wifi/parse.go
Normal file
@ -0,0 +1,30 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
)
|
||||
|
||||
func Parse(packet gopacket.Packet) (ok bool, radio *layers.RadioTap, dot11 *layers.Dot11) {
|
||||
ok = false
|
||||
radio = nil
|
||||
dot11 = nil
|
||||
|
||||
radioLayer := packet.Layer(layers.LayerTypeRadioTap)
|
||||
if radioLayer == nil {
|
||||
return
|
||||
}
|
||||
radio, ok = radioLayer.(*layers.RadioTap)
|
||||
if !ok || radio == nil {
|
||||
return
|
||||
}
|
||||
|
||||
dot11Layer := packet.Layer(layers.LayerTypeDot11)
|
||||
if dot11Layer == nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
dot11, ok = dot11Layer.(*layers.Dot11)
|
||||
return
|
||||
}
|
||||
34
wifi/unpack.go
Normal file
34
wifi/unpack.go
Normal file
@ -0,0 +1,34 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
)
|
||||
|
||||
func Unpack(pkt gopacket.Packet, radio *layers.RadioTap, dot11 *layers.Dot11) (error, []byte) {
|
||||
compressed := false
|
||||
payload := make([]byte, 0)
|
||||
|
||||
for _, layer := range pkt.Layers() {
|
||||
if layer.LayerType() == layers.LayerTypeDot11InformationElement {
|
||||
if info, ok := layer.(*layers.Dot11InformationElement); ok {
|
||||
if info.ID == IDWhisperPayload {
|
||||
payload = append(payload, info.Info...)
|
||||
} else if info.ID == IDWhisperCompression {
|
||||
compressed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if compressed {
|
||||
if decompressed, err := Decompress(payload); err != nil {
|
||||
return fmt.Errorf("error decompressing payload: %v", err), nil
|
||||
} else {
|
||||
payload = decompressed
|
||||
}
|
||||
}
|
||||
|
||||
return nil, payload
|
||||
}
|
||||
41
wifi/utils.go
Normal file
41
wifi/utils.go
Normal file
@ -0,0 +1,41 @@
|
||||
package wifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gopacket/gopacket"
|
||||
"github.com/gopacket/gopacket/layers"
|
||||
)
|
||||
|
||||
func Serialize(layers ...gopacket.SerializableLayer) (error, []byte) {
|
||||
buf := gopacket.NewSerializeBuffer()
|
||||
if err := gopacket.SerializeLayers(buf, SerializationOptions, layers...); err != nil {
|
||||
return err, nil
|
||||
}
|
||||
return nil, buf.Bytes()
|
||||
}
|
||||
|
||||
func IsBroadcast(dot11 *layers.Dot11) bool {
|
||||
return bytes.Equal(dot11.Address1, BroadcastAddr)
|
||||
}
|
||||
|
||||
func Freq2Chan(freq int) int {
|
||||
if freq <= 2472 {
|
||||
return ((freq - 2412) / 5) + 1
|
||||
} else if freq == 2484 {
|
||||
return 14
|
||||
} else if freq >= 5035 && freq <= 5885 {
|
||||
return ((freq - 5035) / 5) + 7
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func Chan2Freq(channel int) int {
|
||||
if channel <= 13 {
|
||||
return ((channel - 1) * 5) + 2412
|
||||
} else if channel == 14 {
|
||||
return 2484
|
||||
} else if channel <= 177 {
|
||||
return ((channel - 7) * 5) + 5035
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user