Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1828b67509 | ||
|
|
8f2d71e631 | ||
|
|
2277089bda | ||
|
|
63f226f68f | ||
|
|
da0e6aa482 | ||
|
|
82a2bb1bd0 | ||
|
|
b96eea97ee | ||
|
|
fd420f4107 | ||
|
|
c5a23a5929 | ||
|
|
eb2e6203be | ||
|
|
b838dc5b31 | ||
|
|
983df4a12e | ||
|
|
9825bad1fe | ||
|
|
0183e5dbcf | ||
|
|
dacc28e2f1 | ||
|
|
90b3abc471 | ||
|
|
09333ebd0d | ||
|
|
e8b1b1ac87 | ||
|
|
eb56a66b20 | ||
|
|
fbd9909fef | ||
|
|
51e76b4c89 | ||
|
|
484a426834 | ||
|
|
31b8840f8c | ||
|
|
dfc13be9cc | ||
|
|
d6bb6a9a1f | ||
|
|
bfbe8f2c1a | ||
|
|
289b0edbb3 | ||
|
|
b69e5f879b | ||
|
|
d8810a449c | ||
|
|
bdcd052500 | ||
|
|
26aa17bf4d | ||
|
|
7a6d6cf2d5 | ||
|
|
e9cdc40821 | ||
|
|
c4c7b420ce | ||
|
|
3d304f13bc | ||
|
|
6ee61c189e | ||
|
|
5ad5666caf | ||
|
|
e057f8e6be | ||
|
|
baf44b4882 | ||
|
|
f7fda8de4c | ||
|
|
43154cf841 | ||
|
|
49f082875d | ||
|
|
1e575e0dbe | ||
|
|
d741020ff8 | ||
|
|
33dca47b38 | ||
|
|
33bd4f9e94 | ||
|
|
7a75a7a255 | ||
|
|
1c23995c86 | ||
|
|
23ae7491e3 | ||
|
|
487578f974 | ||
|
|
4dec06843f | ||
|
|
5f31289430 | ||
|
|
b519619324 | ||
|
|
5ff78297e4 | ||
|
|
c36e0e9a60 | ||
|
|
6e5a41fa34 | ||
|
|
896e13373e | ||
|
|
baeb200a1c | ||
|
|
6648a35f17 | ||
|
|
62204e2f31 | ||
|
|
98c1272874 | ||
|
|
27df923256 | ||
|
|
872d279c49 | ||
|
|
b548446759 | ||
|
|
d5ec02b3d7 | ||
|
|
b60de4547f | ||
|
|
7ab4cbd745 | ||
|
|
51fa7a02ae | ||
|
|
39bcc40f55 | ||
|
|
38c16d62ee | ||
|
|
ecd843c74f | ||
|
|
5611d54131 | ||
|
|
bd5aaaedcd | ||
|
|
c50b770b1e | ||
|
|
80f794b521 | ||
|
|
e22c346537 | ||
|
|
eed2da40b4 | ||
|
|
b5ba669d10 | ||
|
|
832a2608f4 | ||
|
|
ef4b3226ea | ||
|
|
54f2e60695 | ||
|
|
6f1125516c | ||
|
|
76042da044 | ||
|
|
193d855f69 | ||
|
|
65f282ac4c | ||
|
|
5fc97140f3 | ||
|
|
0ce8b70f09 | ||
|
|
b4fbca4e0d | ||
|
|
621bd9304c | ||
|
|
1ab5972f90 | ||
|
|
8aaa5bd167 | ||
|
|
4c89888a67 | ||
|
|
fc4d076113 | ||
|
|
798bd56e75 | ||
|
|
5359d1cb95 | ||
|
|
ff7fe3936b | ||
|
|
d1d221f81e | ||
|
|
e2acdf53a4 | ||
|
|
749d7efab5 | ||
|
|
d96a967fa9 | ||
|
|
3dddd0b041 | ||
|
|
33f02bb832 |
@@ -59,8 +59,11 @@ script:
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
template:
|
||||
- "%{repository_slug}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (%{build_url})"
|
||||
channels:
|
||||
- "ircs://irc.hackint.org:6697/#sn0int"
|
||||
use_notice: true
|
||||
skip_join: true
|
||||
#on_success: change # default: always
|
||||
#on_failure: always # default: always
|
||||
use_notice: true
|
||||
|
||||
4101
Cargo.lock
generated
4101
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sn0int"
|
||||
version = "0.17.0"
|
||||
version = "0.19.1"
|
||||
description = "Semi-automatic OSINT framework and package manager"
|
||||
authors = ["kpcyrd <git@rxv.cc>"]
|
||||
license = "GPL-3.0"
|
||||
@@ -17,12 +17,27 @@ members = ["sn0int-common",
|
||||
"sn0int-registry",
|
||||
"sn0int-std"]
|
||||
|
||||
[package.metadata.deb]
|
||||
extended-description = """\
|
||||
sn0int (pronounced /snoɪnt/) is a semi-automatic OSINT framework and package
|
||||
manager. It was built for IT security professionals and bug hunters to gather
|
||||
intelligence about a given target or about yourself. sn0int is enumerating
|
||||
attack surface by semi-automatically processing public information and mapping
|
||||
the results in a unified format for followup investigations."""
|
||||
section = "utils"
|
||||
priority = "optional"
|
||||
depends = "$auto, publicsuffix"
|
||||
build-depends = "pkg-config, libsqlite3-dev, libseccomp-dev, libsodium-dev, publicsuffix"
|
||||
assets = [
|
||||
["target/release/sn0int", "usr/bin/", "755"],
|
||||
]
|
||||
|
||||
[features]
|
||||
sqlite-bundled = ["libsqlite3-sys/bundled"]
|
||||
|
||||
[dependencies]
|
||||
sn0int-common = { version="0.10.0", path="sn0int-common" }
|
||||
sn0int-std = { version="0.17.0", path="sn0int-std" }
|
||||
sn0int-common = { version="0.11.0", path="sn0int-common" }
|
||||
sn0int-std = { version="=0.19.0", path="sn0int-std" }
|
||||
rustyline = "6.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.7"
|
||||
@@ -35,14 +50,14 @@ lazy_static = "1.0"
|
||||
shellwords = "1.0"
|
||||
diesel = { version = "1.0.0", features = ["sqlite", "chrono"] }
|
||||
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
|
||||
libsqlite3-sys = "0.16.0"
|
||||
libsqlite3-sys = { version = "0.17.0", features = ["bundled-windows"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
dirs = "2.0"
|
||||
url = "2.0"
|
||||
percent-encoding = "2.1"
|
||||
#chrootable-https = { path = "../chrootable-https" }
|
||||
chrootable-https = "0.14"
|
||||
base64 = "0.11"
|
||||
chrootable-https = "0.15.2"
|
||||
base64 = "0.12"
|
||||
data-encoding = "2.1.2"
|
||||
serde_urlencoded = "0.6"
|
||||
serde = "1.0"
|
||||
@@ -62,8 +77,9 @@ semver = "0.9"
|
||||
bytes = "0.4"
|
||||
bytesize = "1.0"
|
||||
ipnetwork = "0.16"
|
||||
strum = "0.17"
|
||||
strum_macros = "0.17"
|
||||
strum = "0.18"
|
||||
strum_macros = "0.18"
|
||||
embedded-triple = "0.1.0"
|
||||
|
||||
digest = "0.8.0"
|
||||
md-5 = "0.8.0"
|
||||
@@ -74,15 +90,17 @@ hmac = "0.7"
|
||||
|
||||
walkdir = "2.2"
|
||||
nude = "0.3"
|
||||
glob = "0.3.0"
|
||||
os-version = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
caps = "0.3"
|
||||
#syscallz = { path="../syscallz-rs" }
|
||||
syscallz = "0.12"
|
||||
syscallz = "0.14.0"
|
||||
nix = "0.17"
|
||||
|
||||
[target.'cfg(target_os="openbsd")'.dependencies]
|
||||
pledge = "0.3.1"
|
||||
pledge = "0.4"
|
||||
unveil = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -1,14 +1,13 @@
|
||||
FROM rust:buster
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM rust:alpine3.11
|
||||
ENV RUSTFLAGS="-C target-feature=-crt-static"
|
||||
RUN apk add --no-cache musl-dev sqlite-dev libseccomp-dev libsodium-dev
|
||||
WORKDIR /usr/src/sn0int
|
||||
COPY . .
|
||||
RUN cargo build --release --verbose
|
||||
RUN strip target/release/sn0int
|
||||
|
||||
FROM debian:buster
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache libgcc sqlite-libs libseccomp libsodium
|
||||
COPY --from=0 /usr/src/sn0int/target/release/sn0int /usr/local/bin/sn0int
|
||||
VOLUME ["/data", "/cache"]
|
||||
ENV XDG_DATA_HOME=/data \
|
||||
|
||||
66
README.md
66
README.md
@@ -58,6 +58,39 @@ Mac OSX
|
||||
|
||||
brew install sn0int
|
||||
|
||||
Debian/Ubuntu/Kali
|
||||
|
||||
There are prebuilt packages signed by a debian maintainer. We can import the
|
||||
key for this repository out of the debian keyring.
|
||||
|
||||
apt install debian-keyring
|
||||
gpg -a --export --keyring /usr/share/keyrings/debian-maintainers.gpg git@rxv.cc | apt-key add -
|
||||
apt-key adv --keyserver keyserver.ubuntu.com --refresh-keys git@rxv.cc
|
||||
echo deb http://apt.vulns.sexy stable main > /etc/apt/sources.list.d/apt-vulns-sexy.list
|
||||
apt update
|
||||
apt install sn0int
|
||||
|
||||
Docker
|
||||
|
||||
docker run --rm --init -it -v "$PWD/.cache:/cache" -v "$PWD/.data:/data" kpcyrd/sn0int
|
||||
|
||||
Alpine
|
||||
|
||||
apk add sn0int
|
||||
|
||||
OpenBSD
|
||||
|
||||
pkg_add sn0int
|
||||
|
||||
Gentoo
|
||||
|
||||
layman -a pentoo
|
||||
emerge --ask net-analyzer/sn0int
|
||||
|
||||
NixOS
|
||||
|
||||
nix-env -i sn0int
|
||||
|
||||
For everything else please have a look at the [detailed list][1].
|
||||
|
||||
[1]: https://sn0int.readthedocs.io/en/latest/install.html
|
||||
@@ -68,10 +101,12 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [Archlinux](https://sn0int.readthedocs.io/en/latest/install.html#archlinux)
|
||||
- [Mac OSX](https://sn0int.readthedocs.io/en/latest/install.html#mac-osx)
|
||||
- [Debian/Ubuntu/Kali](https://sn0int.readthedocs.io/en/latest/install.html#debian-ubuntu-kali)
|
||||
- [Fedora/CentOS/Redhat](https://sn0int.readthedocs.io/en/latest/install.html#fedora-centos-redhat)
|
||||
- [Docker](https://sn0int.readthedocs.io/en/latest/install.html#docker)
|
||||
- [Alpine](https://sn0int.readthedocs.io/en/latest/install.html#alpine)
|
||||
- [OpenBSD](https://sn0int.readthedocs.io/en/latest/install.html#openbsd)
|
||||
- [Gentoo](https://sn0int.readthedocs.io/en/latest/install.html#gentoo)
|
||||
- [NixOS](https://sn0int.readthedocs.io/en/latest/install.html#nixos)
|
||||
- [Windows](https://sn0int.readthedocs.io/en/latest/install.html#windows)
|
||||
- [Build from source](https://sn0int.readthedocs.io/en/latest/build.html)
|
||||
- [Install dependencies](https://sn0int.readthedocs.io/en/latest/build.html#install-dependencies)
|
||||
@@ -89,10 +124,10 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [Running a module](https://sn0int.readthedocs.io/en/latest/usage.html#running-a-module)
|
||||
- [Running followup modules on the results](https://sn0int.readthedocs.io/en/latest/usage.html#running-followup-modules-on-the-results)
|
||||
- [Unscoping entities](https://sn0int.readthedocs.io/en/latest/usage.html#unscoping-entities)
|
||||
- [Autonoscope](https://sn0int.readthedocs.io/en/latest/usage.html#autonoscope)
|
||||
- [Domains](https://sn0int.readthedocs.io/en/latest/usage.html#domains)
|
||||
- [IPs](https://sn0int.readthedocs.io/en/latest/usage.html#ips)
|
||||
- [URLs](https://sn0int.readthedocs.io/en/latest/usage.html#urls)
|
||||
- [Autonoscope](https://sn0int.readthedocs.io/en/latest/autonoscope.html)
|
||||
- [Domains](https://sn0int.readthedocs.io/en/latest/autonoscope.html#domains)
|
||||
- [IPs](https://sn0int.readthedocs.io/en/latest/autonoscope.html#ips)
|
||||
- [URLs](https://sn0int.readthedocs.io/en/latest/autonoscope.html#urls)
|
||||
- [Writing your first module](https://sn0int.readthedocs.io/en/latest/scripting.html)
|
||||
- [Creating a repository](https://sn0int.readthedocs.io/en/latest/scripting.html#creating-a-repository)
|
||||
- [Publish your module](https://sn0int.readthedocs.io/en/latest/scripting.html#publish-your-module)
|
||||
@@ -100,6 +135,8 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [Reading data from stdin](https://sn0int.readthedocs.io/en/latest/scripting.html#reading-data-from-stdin)
|
||||
- [Database](https://sn0int.readthedocs.io/en/latest/database.html)
|
||||
- [db_add](https://sn0int.readthedocs.io/en/latest/database.html#db-add)
|
||||
- [db_add_ttl](https://sn0int.readthedocs.io/en/latest/database.html#db-add-ttl)
|
||||
- [db_activity](https://sn0int.readthedocs.io/en/latest/database.html#db-activity)
|
||||
- [db_update](https://sn0int.readthedocs.io/en/latest/database.html#db-update)
|
||||
- [db_select](https://sn0int.readthedocs.io/en/latest/database.html#db-select)
|
||||
- [Structs](https://sn0int.readthedocs.io/en/latest/structs.html)
|
||||
@@ -127,6 +164,18 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [Logging events](https://sn0int.readthedocs.io/en/latest/activity.html#logging-events)
|
||||
- [Querying events](https://sn0int.readthedocs.io/en/latest/activity.html#querying-events)
|
||||
- [Visualization](https://sn0int.readthedocs.io/en/latest/activity.html#visualization)
|
||||
- [Notifications](https://sn0int.readthedocs.io/en/latest/notifications.html)
|
||||
- [Receiving notifications](https://sn0int.readthedocs.io/en/latest/notifications.html#receiving-notifications)
|
||||
- [Telegram](https://sn0int.readthedocs.io/en/latest/notifications.html#telegram)
|
||||
- [Pushover](https://sn0int.readthedocs.io/en/latest/notifications.html#pushover)
|
||||
- [Discord](https://sn0int.readthedocs.io/en/latest/notifications.html#discord)
|
||||
- [Signal](https://sn0int.readthedocs.io/en/latest/notifications.html#signal)
|
||||
- [Writing your own module](https://sn0int.readthedocs.io/en/latest/notifications.html#writing-your-own-module)
|
||||
- [Setting up notification rules](https://sn0int.readthedocs.io/en/latest/notifications.html#setting-up-notification-rules)
|
||||
- [Testing notifications](https://sn0int.readthedocs.io/en/latest/notifications.html#testing-notifications)
|
||||
- [Running sn0int automatically](https://sn0int.readthedocs.io/en/latest/notifications.html#running-sn0int-automatically)
|
||||
- [Monitors](https://sn0int.readthedocs.io/en/latest/notifications.html#monitors)
|
||||
- [Timers](https://sn0int.readthedocs.io/en/latest/notifications.html#timers)
|
||||
- [Keyring](https://sn0int.readthedocs.io/en/latest/keyring.html)
|
||||
- [Managing the keyring](https://sn0int.readthedocs.io/en/latest/keyring.html#managing-the-keyring)
|
||||
- [Using access keys in scripts](https://sn0int.readthedocs.io/en/latest/keyring.html#using-access-keys-in-scripts)
|
||||
@@ -154,6 +203,7 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [datetime](https://sn0int.readthedocs.io/en/latest/reference.html#datetime)
|
||||
- [db_add](https://sn0int.readthedocs.io/en/latest/reference.html#db-add)
|
||||
- [db_add_ttl](https://sn0int.readthedocs.io/en/latest/reference.html#db-add-ttl)
|
||||
- [db_activity](https://sn0int.readthedocs.io/en/latest/reference.html#db-activity)
|
||||
- [db_select](https://sn0int.readthedocs.io/en/latest/reference.html#db-select)
|
||||
- [db_update](https://sn0int.readthedocs.io/en/latest/reference.html#db-update)
|
||||
- [dns](https://sn0int.readthedocs.io/en/latest/reference.html#dns)
|
||||
@@ -177,12 +227,18 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [img_exif](https://sn0int.readthedocs.io/en/latest/reference.html#img-exif)
|
||||
- [img_nudity](https://sn0int.readthedocs.io/en/latest/reference.html#img-nudity)
|
||||
- [info](https://sn0int.readthedocs.io/en/latest/reference.html#info)
|
||||
- [intval](https://sn0int.readthedocs.io/en/latest/reference.html#intval)
|
||||
- [json_decode](https://sn0int.readthedocs.io/en/latest/reference.html#json-decode)
|
||||
- [json_decode_stream](https://sn0int.readthedocs.io/en/latest/reference.html#json-decode-stream)
|
||||
- [json_encode](https://sn0int.readthedocs.io/en/latest/reference.html#json-encode)
|
||||
- [key_trunc_pad](https://sn0int.readthedocs.io/en/latest/reference.html#key-trunc-pad)
|
||||
- [keyring](https://sn0int.readthedocs.io/en/latest/reference.html#keyring)
|
||||
- [last_err](https://sn0int.readthedocs.io/en/latest/reference.html#last-err)
|
||||
- [md5](https://sn0int.readthedocs.io/en/latest/reference.html#md5)
|
||||
- [mqtt_connect](https://sn0int.readthedocs.io/en/latest/reference.html#mqtt-connect)
|
||||
- [mqtt_subscribe](https://sn0int.readthedocs.io/en/latest/reference.html#mqtt-subscribe)
|
||||
- [mqtt_recv](https://sn0int.readthedocs.io/en/latest/reference.html#mqtt-recv)
|
||||
- [mqtt_ping](https://sn0int.readthedocs.io/en/latest/reference.html#mqtt-ping)
|
||||
- [pgp_pubkey](https://sn0int.readthedocs.io/en/latest/reference.html#pgp-pubkey)
|
||||
- [pgp_pubkey_armored](https://sn0int.readthedocs.io/en/latest/reference.html#pgp-pubkey-armored)
|
||||
- [print](https://sn0int.readthedocs.io/en/latest/reference.html#print)
|
||||
@@ -215,6 +271,7 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [sock_recvuntil](https://sn0int.readthedocs.io/en/latest/reference.html#sock-recvuntil)
|
||||
- [sock_sendafter](https://sn0int.readthedocs.io/en/latest/reference.html#sock-sendafter)
|
||||
- [sock_newline](https://sn0int.readthedocs.io/en/latest/reference.html#sock-newline)
|
||||
- [sodium_secretbox_open](https://sn0int.readthedocs.io/en/latest/reference.html#sodium-secretbox-open)
|
||||
- [status](https://sn0int.readthedocs.io/en/latest/reference.html#status)
|
||||
- [stdin_readline](https://sn0int.readthedocs.io/en/latest/reference.html#stdin-readline)
|
||||
- [stdin_read_to_end](https://sn0int.readthedocs.io/en/latest/reference.html#stdin-read-to-end)
|
||||
@@ -222,6 +279,7 @@ For everything else please have a look at the [detailed list][1].
|
||||
- [str_replace](https://sn0int.readthedocs.io/en/latest/reference.html#str-replace)
|
||||
- [strftime](https://sn0int.readthedocs.io/en/latest/reference.html#strftime)
|
||||
- [strptime](https://sn0int.readthedocs.io/en/latest/reference.html#strptime)
|
||||
- [strval](https://sn0int.readthedocs.io/en/latest/reference.html#strval)
|
||||
- [time_unix](https://sn0int.readthedocs.io/en/latest/reference.html#time-unix)
|
||||
- [url_decode](https://sn0int.readthedocs.io/en/latest/reference.html#url-decode)
|
||||
- [url_encode](https://sn0int.readthedocs.io/en/latest/reference.html#url-encode)
|
||||
|
||||
@@ -47,10 +47,10 @@ def main(tempdir, binary):
|
||||
|
||||
print('[*] installing modules')
|
||||
sn0int(tempdir, binary, [
|
||||
'mod install kpcyrd/ctlogs',
|
||||
'mod install kpcyrd/dns-resolve',
|
||||
'mod install kpcyrd/url-scan',
|
||||
'mod install kpcyrd/geoip',
|
||||
'pkg install kpcyrd/ctlogs',
|
||||
'pkg install kpcyrd/dns-resolve',
|
||||
'pkg install kpcyrd/url-scan',
|
||||
'pkg install kpcyrd/geoip',
|
||||
])
|
||||
|
||||
print('[*] running ctlogs')
|
||||
@@ -63,6 +63,7 @@ def main(tempdir, binary):
|
||||
print('[*] testing db for subdomains')
|
||||
subdomains = sn0int_select(tempdir, binary, ['subdomains'])
|
||||
assert {x['value'] for x in subdomains} == {
|
||||
'example.com',
|
||||
'www.example.com',
|
||||
'm.example.com',
|
||||
'dev.example.com',
|
||||
@@ -91,6 +92,8 @@ def main(tempdir, binary):
|
||||
print('[*] testing db for urls')
|
||||
urls = sn0int_select(tempdir, binary, ['urls'])
|
||||
assert {(x['value'], x['status']) for x in urls} == {
|
||||
('http://example.com/', 200),
|
||||
('https://example.com/', 200),
|
||||
('http://www.example.com/', 200),
|
||||
('https://www.example.com/', 200),
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ case "$1" in
|
||||
cargo test --verbose -- --ignored
|
||||
;;
|
||||
windows)
|
||||
cargo build --verbose --features=sqlite-bundled
|
||||
cargo build --verbose --examples --features=sqlite-bundled
|
||||
cargo build --verbose
|
||||
cargo build --verbose --examples
|
||||
;;
|
||||
boxxy)
|
||||
cargo build --verbose --examples
|
||||
|
||||
@@ -3,6 +3,9 @@ set -exu
|
||||
case "$1" in
|
||||
linux)
|
||||
sudo apt update
|
||||
sudo apt install libsqlite3-dev libseccomp-dev
|
||||
sudo apt install libsqlite3-dev libseccomp-dev libsodium-dev
|
||||
;;
|
||||
osx)
|
||||
brew install libsodium
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
FROM alpine:edge
|
||||
RUN apk add --no-cache sqlite-dev libseccomp-dev
|
||||
RUN apk add --no-cache --virtual .build-rust rust cargo
|
||||
FROM rust:alpine3.11
|
||||
ENV RUSTFLAGS="-C target-feature=-crt-static"
|
||||
RUN apk add --no-cache musl-dev sqlite-dev libseccomp-dev libsodium-dev
|
||||
WORKDIR /usr/src/sn0int
|
||||
COPY . .
|
||||
RUN cargo build --release --verbose
|
||||
RUN strip target/release/sn0int
|
||||
|
||||
FROM alpine:edge
|
||||
RUN apk add --no-cache libgcc sqlite-libs libseccomp
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache libgcc sqlite-libs libseccomp libsodium
|
||||
COPY --from=0 /usr/src/sn0int/target/release/sn0int /usr/local/bin/sn0int
|
||||
VOLUME ["/data", "/cache"]
|
||||
ENV XDG_DATA_HOME=/data \
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
FROM rust
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
|
||||
FROM rust:buster
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev libsodium-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /usr/src/sn0int
|
||||
COPY . .
|
||||
RUN cargo build --release --verbose
|
||||
RUN strip target/release/sn0int
|
||||
|
||||
FROM debian
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev \
|
||||
FROM debian:buster
|
||||
RUN apt-get update -q && apt-get install -yq libsqlite3-dev libseccomp-dev libsodium-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=0 /usr/src/sn0int/target/release/sn0int /usr/local/bin/sn0int
|
||||
VOLUME ["/data", "/cache"]
|
||||
|
||||
81
docs/autonoscope.rst
Normal file
81
docs/autonoscope.rst
Normal file
@@ -0,0 +1,81 @@
|
||||
Autonoscope
|
||||
===========
|
||||
|
||||
Instead of manually unscoping everything you can also define so called
|
||||
autonoscope rules. Those are executed from most specific to least specific and
|
||||
the first match wins. If no rule matches, the default is in-scope::
|
||||
|
||||
[sn0int][demo] > # add the domain first
|
||||
[sn0int][demo] > # this is necessary because we only want to partially unscope example.com
|
||||
[sn0int][demo] > add domain example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > # automatically noscope all subdomains
|
||||
[sn0int][demo] > autonoscope add domain example.com
|
||||
[sn0int][demo] > # except subdomains of prod.example.com
|
||||
[sn0int][demo] > autoscope add domain prod.example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > autonoscope list
|
||||
scope domain "prod.example.com"
|
||||
noscope domain "example.com"
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > # this is going to be out-of-scope
|
||||
[sn0int][demo] > add subdomain www.example.com
|
||||
[sn0int][demo] > # this is going to be in-scope
|
||||
[sn0int][demo] > add subdomain db.prod.example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > select subdomains
|
||||
#1, "www.example.com"
|
||||
#2, "db.prod.example.com"
|
||||
[sn0int][demo] > select subdomains where unscoped=0
|
||||
#2, "db.prod.example.com"
|
||||
[sn0int][demo] > select subdomains where unscoped=1
|
||||
#1, "www.example.com"
|
||||
[sn0int][demo] >
|
||||
|
||||
Domains
|
||||
-------
|
||||
|
||||
Autonoscope rules for domains are applied to the following structs:
|
||||
|
||||
- domains
|
||||
- subdomains
|
||||
- urls
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add domain example.com
|
||||
autonoscope add domain staging.example.com
|
||||
autonoscope add domain com
|
||||
autonoscope add domain .
|
||||
|
||||
IPs
|
||||
---
|
||||
|
||||
Autonoscope rules for IPs are applied to the following structs:
|
||||
|
||||
- ipaddrs
|
||||
- netblocks
|
||||
- ports
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add ip 0.0.0.0/0
|
||||
autonoscope add ip ::/0
|
||||
autonoscope add ip 192.168.0.0/16
|
||||
autonoscope add ip 10.13.33.37/32
|
||||
|
||||
URLs
|
||||
----
|
||||
|
||||
Autonoscope rules for urls are applied to the following structs:
|
||||
|
||||
- urls
|
||||
|
||||
Note that these rules are specific to a certain origin (like
|
||||
``https://example.com``) and are used to filter paths.
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add url https://example.com/
|
||||
autonoscope add url https://example.com/admin/
|
||||
autonoscope add url https://example.com/a/b/c/d
|
||||
@@ -18,19 +18,21 @@ Archlinux
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pacman -S geoip2-database libseccomp publicsuffix-list sqlite
|
||||
$ pacman -S geoip2-database libseccomp libsodium publicsuffix-list sqlite
|
||||
|
||||
Mac OSX
|
||||
~~~~~~~
|
||||
|
||||
None.
|
||||
.. code-block:: bash
|
||||
|
||||
$ brew install libsodium
|
||||
|
||||
Debian/Ubuntu/Kali
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ apt install build-essential libsqlite3-dev libseccomp-dev publicsuffix
|
||||
$ apt install build-essential libsqlite3-dev libseccomp-dev libsodium-dev publicsuffix
|
||||
|
||||
.. warning::
|
||||
On a debian based system make sure you've installed rust with rustup.
|
||||
@@ -40,21 +42,21 @@ Alpine
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ apk add sqlite-dev libseccomp-dev
|
||||
$ apk add sqlite-dev libseccomp-dev libsodium-dev
|
||||
|
||||
OpenBSD
|
||||
~~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pkg_add sqlite3 geolite2-city geolite2-asn
|
||||
$ pkg_add sqlite3 geolite2-city geolite2-asn libsodium
|
||||
|
||||
Gentoo
|
||||
~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
emerge --ask sys-libs/libseccomp dev-db/sqlite
|
||||
emerge --ask sys-libs/libseccomp dev-db/sqlite dev-libs/libsodium
|
||||
|
||||
Windows
|
||||
~~~~~~~
|
||||
@@ -71,10 +73,4 @@ After all dependencies have been installed, simply build the binary:
|
||||
|
||||
$ cargo build --release
|
||||
|
||||
Note that you need a different command on windows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cargo build --release --features=sqlite-bundled
|
||||
|
||||
After the build finished the binary is located at ``target/release/sn0int``.
|
||||
|
||||
@@ -38,10 +38,12 @@ Getting Started
|
||||
install
|
||||
build
|
||||
usage
|
||||
autonoscope
|
||||
scripting
|
||||
database
|
||||
structs
|
||||
activity
|
||||
notifications
|
||||
keyring
|
||||
config
|
||||
sandbox
|
||||
|
||||
@@ -22,9 +22,20 @@ Mac OSX
|
||||
Debian/Ubuntu/Kali
|
||||
------------------
|
||||
|
||||
Note that debian `doesn't ship the geoip2-database
|
||||
<https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=757723>`_ so we're going to
|
||||
download them automatically during the first run.
|
||||
There are prebuilt packages signed by a debian maintainer. We can import the
|
||||
key for this repository out of the debian keyring.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ apt install debian-keyring
|
||||
$ gpg -a --export --keyring /usr/share/keyrings/debian-maintainers.gpg git@rxv.cc | apt-key add -
|
||||
$ apt-key adv --keyserver keyserver.ubuntu.com --refresh-keys git@rxv.cc
|
||||
$ echo deb http://apt.vulns.sexy stable main > /etc/apt/sources.list.d/apt-vulns-sexy.list
|
||||
$ apt update
|
||||
$ apt install sn0int
|
||||
|
||||
Fedora/CentOS/Redhat
|
||||
--------------------
|
||||
|
||||
Using rust+cargo from the repos might work for you, but we only officially
|
||||
support rust+cargo installed with `rustup <https://rustup.rs/>`_. Have a look
|
||||
@@ -32,7 +43,7 @@ at the docker image as an alternative.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ apt install build-essential libsqlite3-dev libseccomp-dev publicsuffix
|
||||
$ dnf install @development-tools libsq3-devel libseccomp-devel libsodium-devel publicsuffix-list
|
||||
$ git clone https://github.com/kpcyrd/sn0int.git
|
||||
$ cd sn0int
|
||||
$ cargo install -f --path .
|
||||
@@ -63,8 +74,15 @@ Gentoo
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
layman -f -o https://raw.githubusercontent.com/kpcyrd/overlay/master/overlay.xml -a kpcyrd-overlay
|
||||
emerge --ask net-analyzer/sn0int
|
||||
$ layman -a pentoo
|
||||
$ emerge --ask net-analyzer/sn0int
|
||||
|
||||
NixOS
|
||||
-----
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ nix-env -i sn0int
|
||||
|
||||
Windows
|
||||
-------
|
||||
@@ -78,4 +96,4 @@ Make sure rust is installed and setup.
|
||||
|
||||
$ git clone https://github.com/kpcyrd/sn0int.git
|
||||
$ cd sn0int
|
||||
$ cargo install -f --path . --features=sqlite-bundled
|
||||
$ cargo install -f --path .
|
||||
|
||||
311
docs/notifications.rst
Normal file
311
docs/notifications.rst
Normal file
@@ -0,0 +1,311 @@
|
||||
Notifications
|
||||
=============
|
||||
|
||||
If you run sn0int unattended nobody might see the sn0int output. For cases like
|
||||
this you can configure notifications to send you a push notification in case
|
||||
something interesting happens. This is also especially useful if you have
|
||||
sn0int setup to run automatically.
|
||||
|
||||
Receiving notifications
|
||||
-----------------------
|
||||
|
||||
Notifications are just regular sn0int modules. You can install them just like
|
||||
any other module or write your own. This section contains walkthroughs on how
|
||||
to setup common integrations.
|
||||
|
||||
Telegram
|
||||
~~~~~~~~
|
||||
|
||||
Install the telegram notification module from the registry:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int pkg install kpcyrd/notify-telegram
|
||||
|
||||
Open your telegram app and open a chat with ``@botfather``. Send ``/newbot``
|
||||
and answer the questions. Copy ``bot_token`` and open this url in your browser:
|
||||
|
||||
.. code-block::
|
||||
|
||||
https://api.telegram.org/bot**your_bot_token**/getUpdates
|
||||
|
||||
Back on your app, open the t.me link to start a new chat with your bot, then
|
||||
send ``/start``. Reload the page in your browser, you should see the new
|
||||
message you sent. Copy the ``chat_id``.
|
||||
|
||||
Test your tokens are working correctly by sending yourself a notification:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec kpcyrd/notify-telegram -o bot_token=1337:foobar -o chat_id=1337 'hello world'
|
||||
|
||||
You should receive ``hello world`` from your bot on Telegram.
|
||||
|
||||
Pushover
|
||||
~~~~~~~~
|
||||
|
||||
Install the pushover notification module from the registry:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int pkg install kpcyrd/notify-pushover
|
||||
|
||||
Signup for pushover and configure the app on your device. Copy th user key
|
||||
visible on the pushover dashboard. Click "Create an Application/API Token". Set
|
||||
"sn0int" as name and set an icon if you want to. Copy the api token.
|
||||
|
||||
Test your tokens are working correctly by sending yourself a notification:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec kpcyrd/notify-pushover -o user_key=asdf1337 -o api_token=asdf1337 'hello world'
|
||||
|
||||
You should receive ``hello world`` as a push notification.
|
||||
|
||||
Discord
|
||||
~~~~~~~
|
||||
|
||||
Install the discord notification module from the registry:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int pkg install kpcyrd/notify-discord
|
||||
|
||||
Decide which channel should receive notifications (or create a new one). Open
|
||||
the "Server Settings" of your discord server. Click on "Webhooks". Click
|
||||
"Create Webhook". Configure the Name and Channel. Copy the Webhook URL.
|
||||
|
||||
|
||||
Test your tokens are working correctly by sending yourself a notification:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec kpcyrd/notify-discord -o url=https://discord.com/api/webhooks/1337/asdf 'hello world'
|
||||
|
||||
You should receive ``hello world`` in your discord channel.
|
||||
|
||||
Signal
|
||||
~~~~~~
|
||||
|
||||
Install the sn0int notification module from the registry:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int pkg install kpcyrd/notify-signal
|
||||
|
||||
This module allows end-to-end encrypted notifications, but it's also difficult
|
||||
to setup. You need a second phone number and install both `signal-cli
|
||||
<https://github.com/AsamK/signal-cli>`_ and `sn0int-signal
|
||||
<https://github.com/kpcyrd/sn0int-signal>`_.
|
||||
|
||||
After you've registered your second phone number with signal-cli, you can use
|
||||
sn0int-signal to expose a minimal api for notify-signal. For more detailed
|
||||
instructions and how to start the api at boot, see the `sn0int-signal README
|
||||
<https://github.com/kpcyrd/sn0int-signal>`_.
|
||||
|
||||
Read the secret key generated at ``/etc/sn0int-signal.key`` and send a
|
||||
notification to the signal phone number:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec kpcyrd/notify-signal -o to=+31337 -o secret=asdf 'hello world'
|
||||
|
||||
You should receive ``hello world`` from the number signed up with signal-cli.
|
||||
|
||||
Writing your own module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Make sure you've read the detailed instructions on how to get setup with
|
||||
`module development <scripting.html>`_.
|
||||
|
||||
Create a new sn0int module like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int new ~/repos/sn0int-modules/notify-custom.lua
|
||||
|
||||
Edit the ``-- Source:`` so it takes notifications as input:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: notifications
|
||||
|
||||
function run(arg)
|
||||
-- TODO your code here
|
||||
-- https://sn0int.readthedocs.io/en/stable/reference.html
|
||||
|
||||
debug(arg)
|
||||
info(arg['subject'])
|
||||
info(arg['body'])
|
||||
end
|
||||
|
||||
Execute your script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec notify-custom 'hello world'
|
||||
|
||||
You most likely need to pass options to avoid hard-coding keys into your
|
||||
script. Options can be fetched like this:
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: notifications
|
||||
|
||||
function run(arg)
|
||||
-- TODO your code here
|
||||
-- https://sn0int.readthedocs.io/en/stable/reference.html
|
||||
|
||||
local foo = getopt('foo')
|
||||
if not foo then return 'Missing -o foo= option' end
|
||||
|
||||
info('foo: ' .. foo)
|
||||
info('subject: ' .. arg['subject'])
|
||||
end
|
||||
|
||||
And passed like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int notify exec notify-custom -o "foo=hello world" 'ohai'
|
||||
|
||||
Setting up notification rules
|
||||
-----------------------------
|
||||
|
||||
We now know how to trigger notifications manually, but we would rather trigger
|
||||
notifications if a module runs into something interesting.
|
||||
|
||||
You can setup subscriptions on specific topics and then have a notification
|
||||
script execute automatically.
|
||||
|
||||
Lookup the location of your sn0int config file:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int paths
|
||||
|
||||
And open it in an editor of your choice:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vim /home/user/.config/sn0int.toml
|
||||
|
||||
A basic configuration could look like this:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
# You can have multiple notification sections, this one is named
|
||||
# `demo-telegram-integration`
|
||||
# The label can be set to whatever you want, but you may need to add
|
||||
# double-quotes to use some characters.
|
||||
[notifications.demo-telegram-integration]
|
||||
# If this option is present, the notification must originate from one of
|
||||
# the following workspaces.
|
||||
workspaces = ["default", "some-workspace"]
|
||||
# If this option is present, the notification must match one of the
|
||||
# filters. You can use `*` as a wildcard to match everything except `:`.
|
||||
topics = ["activity:harness/activity-ping:*"]
|
||||
# Mandatory: the module to execute.
|
||||
script = "kpcyrd/notify-telegram"
|
||||
# The options to pass to the module, if any.
|
||||
# Can be accessed with `getopt`
|
||||
options = [
|
||||
"bot_token=1337:foobar",
|
||||
"chat_id=1337",
|
||||
]
|
||||
|
||||
All options except ``script`` are optional, but setting filters is highly
|
||||
recommended.
|
||||
|
||||
Testing notifications
|
||||
---------------------
|
||||
|
||||
To test if your configuration works correctly you can create an event manually:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sn0int -w some-workspace notify send activity:harness/activity-ping:dummy "hello world"
|
||||
|
||||
If it matches any of your rules you should receive a push notifications.
|
||||
|
||||
.. note::
|
||||
If you want to test just the routing without actually sending something, add ``--dry-run``.
|
||||
|
||||
Running sn0int automatically
|
||||
----------------------------
|
||||
|
||||
Support for this is going to improve in the future, but you can already set
|
||||
this up if you're ok with a slightly buggy experience.
|
||||
|
||||
Monitors
|
||||
~~~~~~~~
|
||||
|
||||
Some modules are long-running and either wait for an event from a server or
|
||||
have custom polling built in that's usually configurable with an ``-o
|
||||
interval=`` option. If your module has a non-trivial setup phase, an author may
|
||||
take this approach.
|
||||
|
||||
.. code-block::
|
||||
|
||||
# /etc/systemd/system/sn0int-your-new-service.service
|
||||
|
||||
[Unit]
|
||||
Description=sn0int: run example/changeme
|
||||
|
||||
[Service]
|
||||
User=your-user
|
||||
ExecStart=/usr/bin/sn0int run -w your-workspace example/changeme
|
||||
|
||||
Restart=always
|
||||
RestartSec=0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
Enable the service to run on boot:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
systemctl enable --now sn0int-your-new-service.service
|
||||
|
||||
Timers
|
||||
~~~~~~
|
||||
|
||||
If the module is only one-shot you can set it up to run with a timer:
|
||||
|
||||
.. code-block::
|
||||
|
||||
# /etc/systemd/system/sn0int-your-other-service.service
|
||||
|
||||
[Unit]
|
||||
Description=sn0int: run example/changeme
|
||||
|
||||
[Service]
|
||||
User=your-user
|
||||
ExecStart=/usr/bin/sn0int run -w your-workspace example/changeme
|
||||
|
||||
Setup the timer like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
# /etc/systemd/system/sn0int-your-other-service.timer
|
||||
|
||||
[Unit]
|
||||
Description=sn0int: run example/changeme
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1h
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
systemctl enable --now sn0int-your-other-service.timer
|
||||
@@ -391,6 +391,8 @@ options are set. The following options are available:
|
||||
``proxy``
|
||||
Use a socks5 proxy in the format ``127.0.0.1:9050``. This option only works
|
||||
if it doesn't conflict with the global proxy settings.
|
||||
``binary``
|
||||
Set to ``true`` to get the http response as raw bytes.
|
||||
|
||||
This function may fail.
|
||||
|
||||
@@ -418,6 +420,8 @@ the following keys:
|
||||
A table of headers
|
||||
``text``
|
||||
The response body as string
|
||||
``binary``
|
||||
The response body as bytes (if ``binary=true``)
|
||||
``blob``
|
||||
If ``into_blob`` was enabled for the request the body is downloaded into blob
|
||||
storage with a reference to the body in this field.
|
||||
@@ -517,6 +521,13 @@ Log an info to the terminal.
|
||||
|
||||
info('ohai')
|
||||
|
||||
intval
|
||||
------
|
||||
|
||||
Parse a number from a string.
|
||||
|
||||
x = strval('1234')
|
||||
|
||||
json_decode
|
||||
-----------
|
||||
|
||||
@@ -551,6 +562,17 @@ Encode a datastructure into a string.
|
||||
})
|
||||
print(x)
|
||||
|
||||
key_trunc_pad
|
||||
-------------
|
||||
|
||||
Truncate/pad a key to a given length.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
-- if longer than 32 bytes: truncate to 32
|
||||
-- if shorter than 32 bytes: pad with \x00
|
||||
local key = key_trunc_pad(password, 32, 0)
|
||||
|
||||
keyring
|
||||
-------
|
||||
|
||||
@@ -585,6 +607,64 @@ Hash a byte array with md5 and return the results as bytes.
|
||||
|
||||
hex(md5("\x00\xff"))
|
||||
|
||||
mqtt_connect
|
||||
------------
|
||||
|
||||
Connect to an mqtt broker.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
local sock = mqtt_connect('mqtts://mqtt.example.com', {
|
||||
username='foo',
|
||||
password='secret',
|
||||
})
|
||||
if last_err() then return end
|
||||
|
||||
mqtt_subscribe
|
||||
--------------
|
||||
|
||||
Subscribe to a topic. Right now only QoS 0 is supported.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
mqtt_subscribe(sock, '#', 0)
|
||||
if last_err() then return end
|
||||
|
||||
mqtt_recv
|
||||
---------
|
||||
|
||||
Receive an mqtt packet. This is not necessarily a publish packet and more
|
||||
packets might be added in the future, so you need to check the type
|
||||
specifically.
|
||||
|
||||
If a read timeout has been set with mqtt_connect_ this function returns ``nil``
|
||||
in case of a read timeout.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
local pkt = mqtt_recv(sock)
|
||||
if last_err() then return end
|
||||
if pkt == nil then
|
||||
-- read timeout, consider sending a ping or disconnect if the previous ping failed
|
||||
elseif pkt['type'] == 'pong' then
|
||||
-- broker sent a pong
|
||||
elseif pkt['type'] == 'publish' then
|
||||
local payload = utf8_decode(pkt['body'])
|
||||
if last_err() then return end
|
||||
info(payload)
|
||||
end
|
||||
|
||||
mqtt_ping
|
||||
---------
|
||||
|
||||
Send a pingreq packet, causing the broker to send a pingresp. This is used to
|
||||
make sure the connection is still working correctly.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
mqtt_ping(sock)
|
||||
if last_err() then return end
|
||||
|
||||
pgp_pubkey
|
||||
----------
|
||||
|
||||
@@ -1012,6 +1092,27 @@ Overwrite the default ``\n`` newline.
|
||||
|
||||
sock_newline(sock, "\r\n")
|
||||
|
||||
sodium_secretbox_open
|
||||
---------------------
|
||||
|
||||
Use authenticated symetric crypto to decrypt a given message.
|
||||
|
||||
Internally this is ``crypto_secretbox_xsalsa20poly1305``.
|
||||
|
||||
The key **must** be 32 bytes, see key_trunc_pad_ if necessary.
|
||||
|
||||
The first 24 bytes of the encrypted message are expected to be the nonce.
|
||||
|
||||
.. code-block:: lua
|
||||
|
||||
plain = sodium_secretbox_open(encrypted, key)
|
||||
if last_err() then return end
|
||||
|
||||
txt = utf8_decode(plain)
|
||||
if last_err() then return end
|
||||
|
||||
info(txt)
|
||||
|
||||
status
|
||||
------
|
||||
|
||||
@@ -1093,6 +1194,13 @@ Parse a date into a unix timestamp, see `strftime rules`_.
|
||||
|
||||
.. _strftime rules: https://docs.rs/chrono/0.4.6/chrono/format/strftime/index.html
|
||||
|
||||
strval
|
||||
------
|
||||
|
||||
Convert a number into a string.
|
||||
|
||||
x = strval(1234)
|
||||
|
||||
time_unix
|
||||
---------
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ child process. It's important to note that there's a basic sandbox that's
|
||||
active on every operating system, and there's a second line of defense on
|
||||
supported operating systems.
|
||||
|
||||
The first line of defense is the restrictive stdlib. It's assumed that and
|
||||
The first line of defense is the restrictive stdlib. It's assumed that an
|
||||
attacker gains full control over the lua code and is able to call any function
|
||||
with arbitrary arguments. The stdlib only provides functions that are
|
||||
considered safe, so for example it's not possible to start a process or open a
|
||||
|
||||
@@ -27,12 +27,12 @@ number of recommended modules::
|
||||
[+] Downloading "GeoLite2-City.mmdb"
|
||||
[+] Downloading "GeoLite2-ASN.mmdb"
|
||||
[+] Loaded 0 modules
|
||||
[*] No modules found, run quickstart to install default modules
|
||||
[*] No modules found, run pkg quickstart to install default modules
|
||||
[sn0int][default] >
|
||||
|
||||
Typing ``quickstart`` is going to get you a fair number of featured modules::
|
||||
Typing ``pkg quickstart`` is going to get you a fair number of featured modules::
|
||||
|
||||
[sn0int][default] > quickstart
|
||||
[sn0int][default] > pkg quickstart
|
||||
[+] Installing kpcyrd/asn
|
||||
[+] Installing kpcyrd/ctlogs
|
||||
[+] Installing kpcyrd/dns-resolve
|
||||
@@ -105,7 +105,7 @@ Running a module
|
||||
Now that we have something to get started with, we can run our first module.
|
||||
First lets list all modules we have::
|
||||
|
||||
[sn0int][demo] > mod list
|
||||
[sn0int][demo] > pkg list
|
||||
kpcyrd/asn (0.1.0)
|
||||
Run a asn lookup for an ip address
|
||||
kpcyrd/ctlogs (0.1.0)
|
||||
@@ -149,7 +149,7 @@ some of them in a browser but hold on, there's a more efficient way to approach
|
||||
this.
|
||||
|
||||
.. hint::
|
||||
You can run the modules concurrently with ``run -j 8``.
|
||||
You can run the modules concurrently with ``run -j3``.
|
||||
|
||||
Running followup modules on the results
|
||||
---------------------------------------
|
||||
@@ -256,85 +256,3 @@ You can reverse this using the scope command::
|
||||
.. hint::
|
||||
All entities have this field, you can refer to it in queries using
|
||||
``unscoped=1``.
|
||||
|
||||
Autonoscope
|
||||
-----------
|
||||
|
||||
Instead of manually unscoping everything you can also define so called
|
||||
autonoscope rules. Those are executed from most specific to least specific and
|
||||
the first match wins. If no rule matches, the default is in-scope::
|
||||
|
||||
[sn0int][demo] > # add the domain first
|
||||
[sn0int][demo] > # this is necessary because we only want to partially unscope example.com
|
||||
[sn0int][demo] > add domain example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > # automatically noscope all subdomains
|
||||
[sn0int][demo] > autonoscope add domain example.com
|
||||
[sn0int][demo] > # except subdomains of prod.example.com
|
||||
[sn0int][demo] > autoscope add domain prod.example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > autonoscope list
|
||||
scope domain "prod.example.com"
|
||||
noscope domain "example.com"
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > # this is going to be out-of-scope
|
||||
[sn0int][demo] > add subdomain www.example.com
|
||||
[sn0int][demo] > # this is going to be in-scope
|
||||
[sn0int][demo] > add subdomain db.prod.example.com
|
||||
[sn0int][demo] >
|
||||
[sn0int][demo] > select subdomains
|
||||
#1, "www.example.com"
|
||||
#2, "db.prod.example.com"
|
||||
[sn0int][demo] > select subdomains where unscoped=0
|
||||
#2, "db.prod.example.com"
|
||||
[sn0int][demo] > select subdomains where unscoped=1
|
||||
#1, "www.example.com"
|
||||
[sn0int][demo] >
|
||||
|
||||
Domains
|
||||
~~~~~~~
|
||||
|
||||
Autonoscope rules for domains are applied to the following structs:
|
||||
|
||||
- domains
|
||||
- subdomains
|
||||
- urls
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add domain example.com
|
||||
autonoscope add domain staging.example.com
|
||||
autonoscope add domain com
|
||||
autonoscope add domain .
|
||||
|
||||
IPs
|
||||
~~~
|
||||
|
||||
Autonoscope rules for IPs are applied to the following structs:
|
||||
|
||||
- ipaddrs
|
||||
- netblocks
|
||||
- ports
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add ip 0.0.0.0/0
|
||||
autonoscope add ip ::/0
|
||||
autonoscope add ip 192.168.0.0/16
|
||||
autonoscope add ip 10.13.33.37/32
|
||||
|
||||
URLs
|
||||
~~~~
|
||||
|
||||
Autonoscope rules for urls are applied to the following structs:
|
||||
|
||||
- urls
|
||||
|
||||
Note that these rules are specific to a certain origin (like
|
||||
``https://example.com``) and are used to filter paths.
|
||||
|
||||
Example rules::
|
||||
|
||||
autonoscope add url https://example.com/
|
||||
autonoscope add url https://example.com/admin/
|
||||
autonoscope add url https://example.com/a/b/c/d
|
||||
|
||||
18
migrations/2020-06-12-222250_ttl-values/down.sql
Normal file
18
migrations/2020-06-12-222250_ttl-values/down.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
CREATE TABLE _ttls_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
family VARCHAR NOT NULL,
|
||||
key INTEGER NOT NULL,
|
||||
expire DATETIME NOT NULL,
|
||||
CONSTRAINT ttl_unique UNIQUE (family, key)
|
||||
);
|
||||
|
||||
INSERT INTO _ttls_new (id, family, key, expire)
|
||||
SELECT id, family, key, expire
|
||||
FROM ttls;
|
||||
|
||||
DROP TABLE ttls;
|
||||
ALTER TABLE _ttls_new RENAME TO ttls;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
||||
19
migrations/2020-06-12-222250_ttl-values/up.sql
Normal file
19
migrations/2020-06-12-222250_ttl-values/up.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
CREATE TABLE _ttls_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
family VARCHAR NOT NULL,
|
||||
key INTEGER NOT NULL,
|
||||
value VARCHAR NOT NULL,
|
||||
expire DATETIME NOT NULL,
|
||||
CONSTRAINT ttl_unique UNIQUE (family, key)
|
||||
);
|
||||
|
||||
INSERT INTO _ttls_new (id, family, key, value, expire)
|
||||
SELECT id, family, key, "unknown", expire
|
||||
FROM ttls;
|
||||
|
||||
DROP TABLE ttls;
|
||||
ALTER TABLE _ttls_new RENAME TO ttls;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
||||
26
modules/harness/activity-ping-once.lua
Normal file
26
modules/harness/activity-ping-once.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Description: Log some dummy activity
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
|
||||
function run()
|
||||
local uniq = getopt('uniq')
|
||||
local topic = getopt('topic') or 'harness/activity-ping:dummy'
|
||||
|
||||
if getopt('gps') then
|
||||
lat=1.23
|
||||
lon=4.56
|
||||
radius=100
|
||||
end
|
||||
|
||||
db_activity({
|
||||
topic=topic,
|
||||
time=sn0int_time(),
|
||||
uniq=uniq,
|
||||
latitude=lat,
|
||||
longitude=lon,
|
||||
radius=radius,
|
||||
content={
|
||||
msg='ohai',
|
||||
},
|
||||
})
|
||||
end
|
||||
26
modules/harness/commit-log.lua
Normal file
26
modules/harness/commit-log.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
|
||||
-- git log -s --format='%H %ci'
|
||||
|
||||
function run()
|
||||
while true do
|
||||
local x = stdin_readline()
|
||||
if x == nil then
|
||||
break
|
||||
end
|
||||
local m = regex_find('^(\\S+) (.+)', x)
|
||||
if m then
|
||||
time = strptime('%Y-%m-%d %T %z', m[3])
|
||||
time = sn0int_time_from(time)
|
||||
|
||||
db_activity({
|
||||
topic='harness/sn0int-commit:dummy',
|
||||
time=time,
|
||||
uniq=m[2],
|
||||
content={},
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
10
modules/harness/dummy-resolve.lua
Normal file
10
modules/harness/dummy-resolve.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: subdomains
|
||||
|
||||
function run(arg)
|
||||
db_update('subdomain', arg, {
|
||||
resolvable=true,
|
||||
})
|
||||
end
|
||||
13
modules/harness/dummy-subdomains.lua
Normal file
13
modules/harness/dummy-subdomains.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: domains
|
||||
|
||||
function run(arg)
|
||||
for i=1, 20 do
|
||||
db_add('subdomain', {
|
||||
domain_id=arg['id'],
|
||||
value=http_mksession() .. '.' .. arg['value'],
|
||||
})
|
||||
end
|
||||
end
|
||||
8
modules/harness/notify-ohai.lua
Normal file
8
modules/harness/notify-ohai.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: notifications
|
||||
|
||||
function run(arg)
|
||||
info('notication!: ' .. json_encode(arg))
|
||||
end
|
||||
9
modules/harness/notify-ratelimit.lua
Normal file
9
modules/harness/notify-ratelimit.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: notifications
|
||||
|
||||
function run(arg)
|
||||
ratelimit_throttle('foo', 3, 10000)
|
||||
info('notication!: ' .. json_encode(arg))
|
||||
end
|
||||
10
modules/harness/notify-slow.lua
Normal file
10
modules/harness/notify-slow.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
-- Source: notifications
|
||||
|
||||
function run(arg)
|
||||
-- TODO your code here
|
||||
sleep(1)
|
||||
info('notication!: ' .. json_encode(arg))
|
||||
end
|
||||
22
modules/harness/pgp-fetch.lua
Normal file
22
modules/harness/pgp-fetch.lua
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
|
||||
function run()
|
||||
session = http_mksession()
|
||||
url = 'https://openpgpkey.archlinux.org/.well-known/openpgpkey/archlinux.org/hu/in9mwr4s84x7gm51851h343n3at1x61g?l=anthraxx'
|
||||
|
||||
req = http_request(session, 'GET', url, {})
|
||||
r = http_fetch(req)
|
||||
-- debug(r)
|
||||
k = pgp_pubkey(r['text'])
|
||||
info(k)
|
||||
|
||||
req = http_request(session, 'GET', url, {
|
||||
binary=true,
|
||||
})
|
||||
r = http_fetch(req)
|
||||
-- debug(r)
|
||||
k = pgp_pubkey(r['binary'])
|
||||
info(k)
|
||||
end
|
||||
58
modules/harness/url-inserter.lua
Normal file
58
modules/harness/url-inserter.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
|
||||
function run()
|
||||
info('preparing')
|
||||
domain_id = db_add('domain', {
|
||||
value='example.com',
|
||||
})
|
||||
subdomain_id = db_add('subdomain', {
|
||||
domain_id=domain_id,
|
||||
value='example.com',
|
||||
})
|
||||
|
||||
info('inserting')
|
||||
url1 = db_add('url', {
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com',
|
||||
})
|
||||
url2 = db_add('url', {
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com/ohai',
|
||||
body='ohai',
|
||||
})
|
||||
url3 = db_add('url', {
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com/world',
|
||||
body={0x77, 0x6f, 0x72, 0x6c, 0x64},
|
||||
})
|
||||
|
||||
info('updating')
|
||||
db_update('url', {
|
||||
id=url1,
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com',
|
||||
path='/',
|
||||
unscoped=false,
|
||||
}, {
|
||||
})
|
||||
db_update('url', {
|
||||
id=url2,
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com/ohai',
|
||||
path='/ohai',
|
||||
body='ohai',
|
||||
unscoped=false,
|
||||
}, {
|
||||
})
|
||||
db_update('url', {
|
||||
id=url3,
|
||||
subdomain_id=subdomain_id,
|
||||
value='https://example.com/world',
|
||||
path='/world',
|
||||
body={0x77, 0x6f, 0x72, 0x6c, 0x64},
|
||||
unscoped=false,
|
||||
}, {
|
||||
})
|
||||
end
|
||||
17
modules/harness/winkekatze-sub.lua
Normal file
17
modules/harness/winkekatze-sub.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Description: TODO your description here
|
||||
-- Version: 0.1.0
|
||||
-- License: GPL-3.0
|
||||
|
||||
function run()
|
||||
local sock = mqtt_connect('mqtt://mqtt.winkekatze24.de', {
|
||||
read_timeout=10,
|
||||
})
|
||||
if last_err() then return end
|
||||
mqtt_subscribe(sock, '#', 0)
|
||||
|
||||
while true do
|
||||
local pkt = mqtt_recv_text(sock)
|
||||
if last_err() then return end
|
||||
info(pkt)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sn0int-common"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
description = "sn0int - common code"
|
||||
authors = ["kpcyrd <git@rxv.cc>"]
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -44,6 +44,7 @@ pub enum Source {
|
||||
Netblocks,
|
||||
CryptoAddrs(Option<String>),
|
||||
KeyRing(String),
|
||||
Notifications,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
@@ -63,6 +64,7 @@ impl Source {
|
||||
Source::Ports => "ports",
|
||||
Source::Netblocks => "netblocks",
|
||||
Source::CryptoAddrs(_) => "cryptoaddrs",
|
||||
Source::Notifications => "notifications",
|
||||
Source::KeyRing(_) => "keyring",
|
||||
}
|
||||
}
|
||||
@@ -94,6 +96,7 @@ impl FromStr for Source {
|
||||
("ports", None) => Ok(Source::Ports),
|
||||
("netblocks", None) => Ok(Source::Netblocks),
|
||||
("cryptoaddrs", param) => Ok(Source::CryptoAddrs(param.map(String::from))),
|
||||
("notifications", None) => Ok(Source::Notifications),
|
||||
("keyring", Some(param)) => Ok(Source::KeyRing(param.to_string())),
|
||||
(x, Some(param)) => bail!("Unknown Source: {:?} ({:?})", x, param),
|
||||
(x, None) => bail!("Unknown Source: {:?}", x),
|
||||
|
||||
@@ -8,7 +8,7 @@ repository = "https://github.com/kpcyrd/sn0int"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
sn0int-common = { version="0.10.0", path="../sn0int-common" }
|
||||
sn0int-common = { version="0.11.0", path="../sn0int-common" }
|
||||
rocket = { version = "0.4", default-features=false }
|
||||
#rocket_failure = { path = "../../rocket_failure" }
|
||||
rocket_failure = { version = "0.2" }
|
||||
|
||||
@@ -2,7 +2,7 @@ use diesel::pg::PgConnection;
|
||||
use oauth2::basic::BasicClient;
|
||||
use oauth2::prelude::*;
|
||||
use oauth2::{AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, TokenUrl, TokenResponse};
|
||||
use crate::github::GithubAuthenticator;
|
||||
use crate::github;
|
||||
use sn0int_registry::errors::*;
|
||||
use sn0int_registry::models::AuthToken;
|
||||
use url::Url;
|
||||
@@ -67,8 +67,7 @@ impl Authenticator {
|
||||
let access_token = response.access_token();
|
||||
let access_token = access_token.secret().to_string();
|
||||
|
||||
let client = GithubAuthenticator::from_env()?;
|
||||
let user = client.get_username(&access_token)?;
|
||||
let user = github::get_username(&access_token)?;
|
||||
|
||||
AuthToken::create(&AuthToken {
|
||||
id: state,
|
||||
|
||||
@@ -4,7 +4,7 @@ use sn0int_registry::db::Connection;
|
||||
use rocket::http::Status;
|
||||
use rocket::{Request, Outcome};
|
||||
use rocket::request::{self, FromRequest};
|
||||
use crate::github::GithubAuthenticator;
|
||||
use crate::github;
|
||||
|
||||
|
||||
pub struct AuthHeader(String);
|
||||
@@ -12,8 +12,7 @@ pub struct AuthHeader(String);
|
||||
impl AuthHeader {
|
||||
pub fn verify(&self, connection: &Connection) -> Result<String> {
|
||||
let session = AuthToken::read(&self.0, &connection)?;
|
||||
let client = GithubAuthenticator::from_env()?;
|
||||
client.get_username(&session.access_token)
|
||||
github::get_username(&session.access_token)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,23 @@
|
||||
use sn0int_registry::errors::*;
|
||||
use std::env;
|
||||
use reqwest;
|
||||
|
||||
pub fn get_username(oauth_token: &str) -> Result<String> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut resp = client.get("https://api.github.com/user")
|
||||
.header("Authorization", format!("token {}", oauth_token))
|
||||
.send()
|
||||
.context("Failed to check access_token")?
|
||||
.error_for_status()
|
||||
.context("Github returned http error")?;
|
||||
|
||||
pub struct GithubAuthenticator {
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
}
|
||||
let data = resp.json::<GithubUser>()
|
||||
.context("Failed to deserialize github reply")?;
|
||||
|
||||
impl GithubAuthenticator {
|
||||
pub fn new(client_id: String, client_secret: String) -> GithubAuthenticator {
|
||||
GithubAuthenticator {
|
||||
client_id,
|
||||
client_secret,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_env() -> Result<GithubAuthenticator> {
|
||||
let client_id = env::var("GITHUB_CLIENT_ID")
|
||||
.context("GITHUB_CLIENT_ID is not set")?;
|
||||
let client_secret = env::var("GITHUB_CLIENT_SECRET")
|
||||
.context("GITHUB_CLIENT_SECRET is not set")?;
|
||||
Ok(GithubAuthenticator::new(client_id, client_secret))
|
||||
}
|
||||
|
||||
pub fn get_username(&self, oauth_token: &str) -> Result<String> {
|
||||
let url = format!("https://api.github.com/applications/{}/tokens/{}", self.client_id, oauth_token);
|
||||
let client = reqwest::Client::new();
|
||||
let mut resp = client.get(&url)
|
||||
.basic_auth(&self.client_id, Some(&self.client_secret))
|
||||
.send()?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
bail!("Github returned: {}", resp.status())
|
||||
}
|
||||
|
||||
let data = resp.json::<GithubReply>()
|
||||
.context("Failed to deserialize github reply")?;
|
||||
|
||||
Ok(data.user.login)
|
||||
}
|
||||
Ok(data.login)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GithubReply {
|
||||
user: GithubUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GithubUser {
|
||||
struct GithubUser {
|
||||
login: String,
|
||||
#[serde(rename="type")]
|
||||
user_type: String,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
To install a module run:
|
||||
</p>
|
||||
<p class="code"><code>
|
||||
sn0int install kpcyrd/ctlogs
|
||||
sn0int pkg install kpcyrd/ctlogs
|
||||
</code></p>
|
||||
|
||||
{{#each modules}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sn0int-std"
|
||||
version = "0.17.0"
|
||||
version = "0.19.0"
|
||||
description = "sn0int - stdlib"
|
||||
authors = ["kpcyrd <git@rxv.cc>"]
|
||||
repository = "https://github.com/kpcyrd/sn0int"
|
||||
@@ -24,22 +24,25 @@ serde_urlencoded = "0.6"
|
||||
webpki = "0.21"
|
||||
webpki-roots = "0.19.0"
|
||||
ct-logs = "0.6"
|
||||
chrootable-https = "0.14"
|
||||
#chrootable-https = { path = "../../chrootable-https" }
|
||||
chrootable-https = "0.15.2"
|
||||
http = "0.2"
|
||||
bufstream = "0.1.4"
|
||||
pem = "0.7"
|
||||
url = "2.0"
|
||||
tungstenite = { version = "0.10.1", default-features = false }
|
||||
kuchiki = "0.8.0"
|
||||
maxminddb = "0.13"
|
||||
x509-parser = "0.6.0"
|
||||
maxminddb = "0.14"
|
||||
x509-parser = "0.7"
|
||||
der-parser = "3.0"
|
||||
publicsuffix = { version="1.5", default-features=false }
|
||||
xml-rs = "0.8"
|
||||
geo = "0.12"
|
||||
geo = "0.13"
|
||||
bytes = "0.4"
|
||||
base64 = "0.11"
|
||||
base64 = "0.12"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
mqtt-protocol = "0.8.1"
|
||||
sodiumoxide = { version="0.2.5", features=["use-pkg-config"] }
|
||||
|
||||
image = "0.23.0"
|
||||
kamadak-exif = "0.5.1"
|
||||
|
||||
51
sn0int-std/src/crypto.rs
Normal file
51
sn0int-std/src/crypto.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use crate::errors::*;
|
||||
use sodiumoxide::crypto::secretbox::{self, Key, Nonce};
|
||||
use std::iter;
|
||||
|
||||
pub fn key_trunc_pad(mut key: &[u8], len: usize, pad: u8) -> Vec<u8> {
|
||||
if key.len() > len {
|
||||
key = &key[..len];
|
||||
}
|
||||
|
||||
let mut key = key.to_vec();
|
||||
key.extend(iter::repeat(pad).take(len - key.len()));
|
||||
key
|
||||
}
|
||||
|
||||
pub fn sodium_secretbox_open(encrypted: &[u8], key: &[u8]) -> Result<Vec<u8>> {
|
||||
if encrypted.len() <= secretbox::NONCEBYTES {
|
||||
bail!("Encrypted message is too short");
|
||||
}
|
||||
|
||||
let key = Key::from_slice(key)
|
||||
.ok_or_else(|| format_err!("Key has wrong length"))?;
|
||||
let nonce = Nonce::from_slice(&encrypted[..secretbox::NONCEBYTES])
|
||||
.ok_or_else(|| format_err!("Nonce has wrong length"))?;
|
||||
let ciphertext = &encrypted[secretbox::NONCEBYTES..];
|
||||
let plain = secretbox::open(&ciphertext, &nonce, &key)
|
||||
.map_err(|_| format_err!("Failed to decrypt secretbox"))?;
|
||||
Ok(plain)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_key_equal() {
|
||||
let key = key_trunc_pad(&[1, 2, 3, 4, 5], 5, 0);
|
||||
assert_eq!(key, &[1, 2, 3, 4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_trunc() {
|
||||
let key = key_trunc_pad(&[1, 2, 3, 4, 5, 6, 7, 8, 9], 5, 0);
|
||||
assert_eq!(key, &[1, 2, 3, 4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_pad() {
|
||||
let key = key_trunc_pad(&[1, 2, 3], 5, 0);
|
||||
assert_eq!(key, &[1, 2, 3, 0, 0]);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,12 @@ impl LuaMap {
|
||||
pub fn insert_num<K: Into<String>>(&mut self, k: K, v: f64) {
|
||||
self.0.insert(AnyHashableLuaValue::LuaString(k.into()), AnyLuaValue::LuaNumber(v));
|
||||
}
|
||||
|
||||
pub fn insert_serde<K: Into<String>, S: serde::Serialize>(&mut self, k: K, v: S) -> Result<()> {
|
||||
let v = serde_json::to_value(v)?;
|
||||
self.0.insert(AnyHashableLuaValue::LuaString(k.into()), LuaJsonValue::from(v).into());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for LuaMap {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::errors::*;
|
||||
use crate::lazy::LazyInit;
|
||||
// use crate::paths;
|
||||
use maxminddb::{self, geoip2};
|
||||
use std::fmt;
|
||||
use std::fs::{self, File};
|
||||
@@ -26,6 +25,8 @@ pub trait Maxmind: Sized {
|
||||
"/usr/share/GeoIP/",
|
||||
// OpenBSD
|
||||
"/usr/local/share/examples/libmaxminddb/",
|
||||
// geoipupdate
|
||||
"/var/lib/GeoIP/",
|
||||
] {
|
||||
let path = Path::new(path);
|
||||
let path = path.join(Self::filename());
|
||||
|
||||
@@ -3,14 +3,8 @@ use maxminddb::geoip2;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
||||
fn from_geoip_model_names(names: Option<BTreeMap<String, String>>) -> Option<String> {
|
||||
let names = match names {
|
||||
Some(names) => names,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
names.get("en")
|
||||
.map(|x| x.to_owned())
|
||||
fn from_geoip_model_names(names: Option<BTreeMap<&str, &str>>) -> Option<String> {
|
||||
names?.get("en").map(|x| x.to_string())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -24,7 +18,7 @@ pub struct GeoLookup {
|
||||
pub longitude: Option<f64>,
|
||||
}
|
||||
|
||||
impl From<geoip2::City> for GeoLookup {
|
||||
impl<'a> From<geoip2::City<'a>> for GeoLookup {
|
||||
fn from(lookup: geoip2::City) -> GeoLookup {
|
||||
// parse maxminddb lookup
|
||||
let continent = match lookup.continent {
|
||||
@@ -79,17 +73,11 @@ pub struct Continent {
|
||||
|
||||
impl Continent {
|
||||
fn from_maxmind(continent: geoip2::model::Continent) -> Option<Self> {
|
||||
let code = match continent.code {
|
||||
Some(code) => code,
|
||||
_ => return None,
|
||||
};
|
||||
let name = match from_geoip_model_names(continent.names) {
|
||||
Some(name) => name,
|
||||
_ => return None,
|
||||
};
|
||||
let code = continent.code?;
|
||||
let name = from_geoip_model_names(continent.names)?;
|
||||
|
||||
Some(Continent {
|
||||
code,
|
||||
code: code.to_string(),
|
||||
name,
|
||||
})
|
||||
}
|
||||
@@ -103,17 +91,11 @@ pub struct Country {
|
||||
|
||||
impl Country {
|
||||
fn from_maxmind(country: geoip2::model::Country) -> Option<Self> {
|
||||
let code = match country.iso_code {
|
||||
Some(code) => code,
|
||||
_ => return None,
|
||||
};
|
||||
let name = match from_geoip_model_names(country.names) {
|
||||
Some(name) => name,
|
||||
_ => return None,
|
||||
};
|
||||
let code = country.iso_code?;
|
||||
let name = from_geoip_model_names(country.names)?;
|
||||
|
||||
Some(Country {
|
||||
code,
|
||||
code: code.to_string(),
|
||||
name,
|
||||
})
|
||||
}
|
||||
@@ -152,19 +134,14 @@ pub struct AsnLookup {
|
||||
impl AsnLookup {
|
||||
pub fn try_from(lookup: geoip2::Isp) -> Result<AsnLookup> {
|
||||
// parse maxminddb lookup
|
||||
let asn = match lookup.autonomous_system_number {
|
||||
Some(asn) => asn,
|
||||
_ => bail!("autonomous_system_number not set"),
|
||||
};
|
||||
let as_org = match lookup.autonomous_system_organization {
|
||||
Some(org) => org,
|
||||
_ => bail!("autonomous_system_organization not set"),
|
||||
};
|
||||
let asn = lookup.autonomous_system_number
|
||||
.ok_or_else(|| format_err!("autonomous_system_number not set"))?;
|
||||
let as_org = lookup.autonomous_system_organization
|
||||
.ok_or_else(|| format_err!("autonomous_system_organization not set"))?;
|
||||
|
||||
// return result
|
||||
Ok(AsnLookup {
|
||||
asn,
|
||||
as_org,
|
||||
as_org: as_org.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ use hlua_badtouch as hlua;
|
||||
|
||||
pub mod blobs;
|
||||
pub mod crt;
|
||||
pub mod crypto;
|
||||
mod errors;
|
||||
pub mod engine;
|
||||
pub mod geo;
|
||||
pub mod geoip;
|
||||
pub mod gfx;
|
||||
pub mod html;
|
||||
pub mod ipc;
|
||||
pub mod json;
|
||||
pub mod lazy;
|
||||
pub mod mqtt;
|
||||
pub mod psl;
|
||||
pub mod ratelimits;
|
||||
pub mod sockets;
|
||||
|
||||
228
sn0int-std/src/mqtt.rs
Normal file
228
sn0int-std/src/mqtt.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use chrootable_https::DnsResolver;
|
||||
use crate::errors::*;
|
||||
use crate::hlua::AnyLuaValue;
|
||||
use mqtt::packet::VariablePacketError;
|
||||
use crate::json::LuaJsonValue;
|
||||
use crate::sockets::{Stream, SocketOptions};
|
||||
use mqtt::{TopicFilter, QualityOfService};
|
||||
use mqtt::control::ConnectReturnCode;
|
||||
use mqtt::control::fixed_header::FixedHeaderError;
|
||||
use mqtt::encodable::{Encodable, Decodable};
|
||||
use mqtt::packet::{Packet, VariablePacket, ConnectPacket, SubscribePacket, PingreqPacket};
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct MqttOptions {
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
|
||||
pub proxy: Option<SocketAddr>,
|
||||
#[serde(default)]
|
||||
pub connect_timeout: u64,
|
||||
#[serde(default)]
|
||||
pub read_timeout: u64,
|
||||
#[serde(default)]
|
||||
pub write_timeout: u64,
|
||||
}
|
||||
|
||||
impl MqttOptions {
|
||||
pub fn try_from(x: AnyLuaValue) -> Result<MqttOptions> {
|
||||
let x = LuaJsonValue::from(x);
|
||||
let x = serde_json::from_value(x.into())?;
|
||||
Ok(x)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MqttClient {
|
||||
stream: Stream,
|
||||
}
|
||||
|
||||
impl MqttClient {
|
||||
pub fn negotiate(stream: Stream, options: &MqttOptions) -> Result<MqttClient> {
|
||||
let mut client = MqttClient {
|
||||
stream,
|
||||
};
|
||||
|
||||
let mut pkt = ConnectPacket::new("MQTT", "sn0int");
|
||||
pkt.set_user_name(options.username.clone());
|
||||
pkt.set_password(options.password.clone());
|
||||
|
||||
/*
|
||||
if let Some(keep_alive) = msg.keep_alive {
|
||||
packet.set_keep_alive(keep_alive);
|
||||
}
|
||||
*/
|
||||
|
||||
client.send(pkt.into())?;
|
||||
let pkt = client.recv()?;
|
||||
|
||||
if let VariablePacket::ConnackPacket(pkt) = pkt {
|
||||
let code = pkt.connect_return_code();
|
||||
if code == ConnectReturnCode::ConnectionAccepted {
|
||||
Ok(client)
|
||||
} else {
|
||||
bail!("MQTT negotiation failed: {:?}", code);
|
||||
}
|
||||
} else {
|
||||
bail!("Expected ConnAck, received {:?}", pkt);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect<R: DnsResolver>(resolver: &R, url: Url, options: &MqttOptions) -> Result<MqttClient> {
|
||||
let tls = match url.scheme() {
|
||||
"mqtt" => false,
|
||||
"mqtts" => true,
|
||||
_ => bail!("Invalid mqtt protocol"),
|
||||
};
|
||||
|
||||
let host = url.host_str()
|
||||
.ok_or_else(|| format_err!("Missing host in url"))?;
|
||||
|
||||
let port = match (url.port(), tls) {
|
||||
(Some(port), _) => port,
|
||||
(None, true) => 8883,
|
||||
(None, false) => 1883,
|
||||
};
|
||||
|
||||
|
||||
let stream = Stream::connect_stream(resolver, host, port, &SocketOptions {
|
||||
tls,
|
||||
sni_value: None,
|
||||
disable_tls_verify: false,
|
||||
proxy: options.proxy,
|
||||
|
||||
connect_timeout: options.connect_timeout,
|
||||
read_timeout: options.read_timeout,
|
||||
write_timeout: options.write_timeout,
|
||||
})?;
|
||||
|
||||
Self::negotiate(stream, options)
|
||||
}
|
||||
|
||||
fn send(&mut self, pkt: VariablePacket) -> Result<()> {
|
||||
debug!("Sending mqtt packet: {:?}", pkt);
|
||||
pkt.encode(&mut self.stream)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> std::result::Result<VariablePacket, VariablePacketError> {
|
||||
let pkt = VariablePacket::decode(&mut self.stream)?;
|
||||
debug!("Received mqtt packet: {:?}", pkt);
|
||||
Ok(pkt)
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, topic: &str, qos: u8) -> Result<()> {
|
||||
let filter = TopicFilter::new(topic)?;
|
||||
|
||||
let qos = match qos {
|
||||
0 => QualityOfService::Level0,
|
||||
1 => QualityOfService::Level1,
|
||||
2 => QualityOfService::Level2,
|
||||
_ => bail!("Invalid QoS level: {}", qos),
|
||||
};
|
||||
|
||||
let pkt = SubscribePacket::new(1, vec![(filter, qos)]);
|
||||
self.send(pkt.into())?;
|
||||
|
||||
let pkt = self.recv()?;
|
||||
if let VariablePacket::SubackPacket(_pkt) = pkt {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Expected SubAck, received {:?}", pkt);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_pkt(&mut self) -> Result<Option<Pkt>> {
|
||||
match self.recv() {
|
||||
Ok(pkt) => Ok(Some(Pkt::try_from(pkt)?)),
|
||||
Err(VariablePacketError::IoError(err)) if err.kind() == io::ErrorKind::WouldBlock => Ok(None),
|
||||
Err(VariablePacketError::FixedHeaderError(FixedHeaderError::IoError(err))) if err.kind() == io::ErrorKind::WouldBlock => Ok(None),
|
||||
Err(err) => Err(Error::from(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ping(&mut self) -> Result<()> {
|
||||
let pkt = PingreqPacket::new();
|
||||
self.send(pkt.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Pkt {
|
||||
#[serde(rename="publish")]
|
||||
Publish(Publish),
|
||||
#[serde(rename="pong")]
|
||||
Pong,
|
||||
}
|
||||
|
||||
impl Pkt {
|
||||
pub fn to_lua(&self) -> Result<AnyLuaValue> {
|
||||
let v = serde_json::to_value(&self)?;
|
||||
let v = LuaJsonValue::from(v).into();
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<VariablePacket> for Pkt {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(pkt: VariablePacket) -> Result<Pkt> {
|
||||
match pkt {
|
||||
VariablePacket::ConnectPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::ConnackPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PublishPacket(pkt) => Ok(Pkt::Publish(Publish {
|
||||
topic: pkt.topic_name().to_string(),
|
||||
body: pkt.payload(),
|
||||
})),
|
||||
VariablePacket::PubackPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PubrecPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PubrelPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PubcompPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PingreqPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::PingrespPacket(_) => Ok(Pkt::Pong),
|
||||
VariablePacket::SubscribePacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::SubackPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::UnsubscribePacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::UnsubackPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
VariablePacket::DisconnectPacket(_) => bail!("Unsupported pkt: {:?}", pkt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Publish {
|
||||
pub topic: String,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrootable_https::dns::Resolver;
|
||||
|
||||
fn connect() -> Result<MqttClient> {
|
||||
let resolver = Resolver::from_system_v4().unwrap();
|
||||
let url = "mqtt://mqtt.winkekatze24.de".parse()?;
|
||||
MqttClient::connect(&resolver, url, &MqttOptions::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_connect() {
|
||||
connect().expect("Failed to setup connection");
|
||||
}
|
||||
|
||||
// this test is too flaky
|
||||
/*
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_subscribe() {
|
||||
let mut c = connect().unwrap();
|
||||
c.subscribe("#", 0).unwrap();
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -60,7 +60,7 @@ impl PslReader {
|
||||
}
|
||||
|
||||
pub fn download(path: &Path, url: &str) -> Result<()> {
|
||||
let client = Client::with_system_resolver()?;
|
||||
let client = Client::with_system_resolver_v4()?;
|
||||
let resp = client.get(url)
|
||||
.wait_for_response()
|
||||
.context("http request failed")?;
|
||||
|
||||
@@ -319,6 +319,7 @@ impl Socket {
|
||||
let available = match self.stream.fill_buf() {
|
||||
Ok(n) => n,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(Vec::new()),
|
||||
Err(e) => return Err(e.into())
|
||||
};
|
||||
|
||||
@@ -363,7 +364,7 @@ mod tests {
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn verify_tls_good() {
|
||||
let resolver = Resolver::from_system().unwrap();
|
||||
let resolver = Resolver::from_system_v4().unwrap();
|
||||
let _sock = Socket::connect(&resolver, "badssl.com", 443, &SocketOptions{
|
||||
tls: true,
|
||||
..Default::default()
|
||||
@@ -373,7 +374,7 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn verify_tls_good_request() {
|
||||
let resolver = Resolver::from_system().unwrap();
|
||||
let resolver = Resolver::from_system_v4().unwrap();
|
||||
let mut sock = Socket::connect(&resolver, "badssl.com", 443, &SocketOptions{
|
||||
tls: true,
|
||||
..Default::default()
|
||||
@@ -386,7 +387,7 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn verify_tls_expired() {
|
||||
let resolver = Resolver::from_system().unwrap();
|
||||
let resolver = Resolver::from_system_v4().unwrap();
|
||||
let sock = Socket::connect(&resolver, "expired.badssl.com", 443, &SocketOptions{
|
||||
tls: true,
|
||||
..Default::default()
|
||||
@@ -400,7 +401,7 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn verify_tls_1_1_1_1() {
|
||||
let resolver = Resolver::from_system().unwrap();
|
||||
let resolver = Resolver::from_system_v4().unwrap();
|
||||
let _sock = Socket::connect(&resolver, "1.1.1.1", 443, &SocketOptions{
|
||||
tls: true,
|
||||
..Default::default()
|
||||
|
||||
@@ -72,6 +72,8 @@ pub struct RequestOptions {
|
||||
#[serde(default)]
|
||||
into_blob: bool,
|
||||
proxy: Option<SocketAddr>,
|
||||
#[serde(default)]
|
||||
binary: bool,
|
||||
}
|
||||
|
||||
impl RequestOptions {
|
||||
@@ -101,6 +103,7 @@ pub struct HttpRequest {
|
||||
timeout: Option<Duration>,
|
||||
into_blob: bool,
|
||||
proxy: Option<SocketAddr>,
|
||||
binary: bool,
|
||||
}
|
||||
|
||||
impl HttpRequest {
|
||||
@@ -123,6 +126,7 @@ impl HttpRequest {
|
||||
timeout,
|
||||
into_blob: options.into_blob,
|
||||
proxy: options.proxy,
|
||||
binary: options.binary,
|
||||
};
|
||||
|
||||
if let Some(json) = options.json {
|
||||
@@ -255,6 +259,8 @@ impl HttpRequest {
|
||||
let blob = Blob::create(res.body);
|
||||
let id = state.register_blob(blob);
|
||||
resp.insert_str("blob", id);
|
||||
} else if self.binary {
|
||||
resp.insert_serde("binary", &res.body[..])?;
|
||||
} else {
|
||||
resp.insert_str("text", String::from_utf8_lossy(&res.body));
|
||||
}
|
||||
|
||||
26
src/api.rs
26
src/api.rs
@@ -18,18 +18,27 @@ pub struct Client {
|
||||
server: String,
|
||||
client: chrootable_https::Client<chrootable_https::Resolver>,
|
||||
session: Option<String>,
|
||||
os_version: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(config: &Config) -> Result<Client> {
|
||||
let client = match config.network.proxy {
|
||||
Some(proxy) => chrootable_https::Client::with_socks5(proxy),
|
||||
_ => chrootable_https::Client::with_system_resolver()?,
|
||||
let client = if let Some(proxy) = config.network.proxy {
|
||||
chrootable_https::Client::with_socks5(proxy)
|
||||
} else {
|
||||
chrootable_https::Client::with_system_resolver_v4()?
|
||||
};
|
||||
|
||||
let os_version = match os_version::detect() {
|
||||
Ok(v) => v.to_string(),
|
||||
Err(_) => String::from("unknown"),
|
||||
};
|
||||
|
||||
Ok(Client {
|
||||
server: config.core.registry.clone(),
|
||||
client,
|
||||
session: None,
|
||||
os_version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,12 +50,21 @@ impl Client {
|
||||
thread_rng().sample_iter(&Alphanumeric).take(32).collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn user_agent(&self) -> String {
|
||||
format!("{}, {}, {}",
|
||||
web::default_user_agent(),
|
||||
embedded_triple::get(),
|
||||
self.os_version,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn request<T: DeserializeOwned + fmt::Debug>(&self, mut request: RequestBuilder, body: Body) -> Result<T> {
|
||||
if let Some(session) = &self.session {
|
||||
info!("Adding session token to request");
|
||||
request.header("Auth", session.as_str());
|
||||
}
|
||||
request.header("User-Agent", web::default_user_agent());
|
||||
request.header("User-Agent", self.user_agent());
|
||||
|
||||
let request = request.body(body)?;
|
||||
|
||||
|
||||
@@ -72,6 +72,12 @@ pub enum SubCommand {
|
||||
/// Manage workspaces
|
||||
#[structopt(name="workspace")]
|
||||
Workspace(cmd::workspace_cmd::Args),
|
||||
/// Calendar
|
||||
#[structopt(name="cal")]
|
||||
Cal(cmd::cal_cmd::Args),
|
||||
/// Notify
|
||||
#[structopt(name="notify")]
|
||||
Notify(cmd::notify_cmd::Args),
|
||||
/// Verify blob storage for corrupt and dangling blobs
|
||||
#[structopt(name="fsck")]
|
||||
Fsck(cmd::fsck_cmd::Args),
|
||||
@@ -124,6 +130,9 @@ pub struct Run {
|
||||
/// Narrow down targeted entities
|
||||
#[structopt(short="t", long="target")]
|
||||
pub target: Option<String>,
|
||||
/// Dump the sandbox init message to stdout instead of running a child process
|
||||
#[structopt(long="dump-sandbox-init-msg")]
|
||||
pub dump_sandbox_init_msg: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
||||
397
src/cal/date.rs
Normal file
397
src/cal/date.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use chrono::Utc;
|
||||
use chrono::prelude::*;
|
||||
use crate::cal::{ActivityGrade, DateArg};
|
||||
use crate::models::*;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
const MONTH_LINES: i32 = 7;
|
||||
|
||||
fn merge_months(ctx: &DateContext, months: &[DateSpec]) -> String {
|
||||
let mut months = months.iter()
|
||||
.map(|ds| {
|
||||
let month = ds.to_term_string(ctx);
|
||||
month.lines()
|
||||
.map(String::from)
|
||||
.collect::<VecDeque<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut out = String::new();
|
||||
for i in 0..=MONTH_LINES {
|
||||
let mut first = true;
|
||||
for m in &mut months {
|
||||
if !first {
|
||||
out.push_str(" ");
|
||||
}
|
||||
if let Some(line) = m.pop_front() {
|
||||
out.push_str(&line);
|
||||
} else {
|
||||
out.push_str(&" ".repeat(21));
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
if i < MONTH_LINES {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn chunk_months(ctx: &DateContext, months: &[DateSpec]) -> String {
|
||||
months
|
||||
.chunks(3)
|
||||
.map(|m| merge_months(ctx, m))
|
||||
.fold(String::new(), |a, b| {
|
||||
if a.is_empty() {
|
||||
a + &b
|
||||
} else {
|
||||
a + "\n" + &b
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn days_in_month(year: i32, month: u32) -> i64 {
|
||||
let start = Utc.ymd(year, month, 1);
|
||||
let end = if month == 12 {
|
||||
Utc.ymd(year + 1, 1, 1)
|
||||
} else {
|
||||
Utc.ymd(year, month + 1, 1)
|
||||
};
|
||||
end.signed_duration_since(start).num_days()
|
||||
}
|
||||
|
||||
fn setup_graph_map(events: &[Activity]) -> (HashMap<NaiveDate, u64>, u64) {
|
||||
debug!("Found {} events in selected range", events.len());
|
||||
|
||||
let mut cur = None;
|
||||
let mut ctr = 0;
|
||||
let mut max = 0;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
for event in events {
|
||||
let date = event.time.date();
|
||||
if let Some(cur) = cur.as_mut() {
|
||||
if date == *cur {
|
||||
ctr += 1;
|
||||
} else {
|
||||
if ctr > max {
|
||||
max = ctr;
|
||||
}
|
||||
map.insert(cur.clone(), ctr);
|
||||
*cur = date;
|
||||
ctr = 1;
|
||||
}
|
||||
} else {
|
||||
cur = Some(date);
|
||||
ctr = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ctr > 0 {
|
||||
if let Some(cur) = cur.take() {
|
||||
if ctr > max {
|
||||
max = ctr;
|
||||
}
|
||||
map.insert(cur, ctr);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Maximum events per day is {}", max);
|
||||
|
||||
(map, max)
|
||||
}
|
||||
|
||||
pub struct DateContext {
|
||||
events: HashMap<NaiveDate, u64>,
|
||||
max: u64,
|
||||
today: NaiveDate,
|
||||
}
|
||||
|
||||
impl DateContext {
|
||||
pub fn new(events: &[Activity], today: NaiveDate) -> DateContext {
|
||||
let (events, max) = setup_graph_map(&events);
|
||||
DateContext {
|
||||
events,
|
||||
max,
|
||||
today,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_today(&self, date: &NaiveDate) -> bool {
|
||||
self.today == *date
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_future(&self, date: &NaiveDate) -> bool {
|
||||
self.today < *date
|
||||
}
|
||||
|
||||
fn activity_for_day(&self, date: &NaiveDate) -> ActivityGrade {
|
||||
if let Some(events) = self.events.get(date) {
|
||||
ActivityGrade::from_ratio(*events, self.max)
|
||||
} else {
|
||||
ActivityGrade::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DateSpec {
|
||||
Year(i32),
|
||||
YearMonth((i32, u32)),
|
||||
YearMonthContext((i32, u32, u32)),
|
||||
}
|
||||
|
||||
impl DateSpec {
|
||||
pub fn from_args(args: &[DateArg], context: Option<u32>) -> Result<DateSpec> {
|
||||
if args.len() > 2 {
|
||||
bail!("Too many datespec args");
|
||||
}
|
||||
|
||||
let today = Utc::today();
|
||||
let ds = match (args.get(0), args.get(1), context) {
|
||||
(None, _, None) => DateSpec::YearMonth((today.year(), today.month())),
|
||||
(None, _, Some(context)) => DateSpec::YearMonthContext((today.year(), today.month(), context)),
|
||||
|
||||
(Some(DateArg::Month(month)), None, None) => DateSpec::YearMonth((today.year(), *month)),
|
||||
(Some(DateArg::Num(year)), None, None) => DateSpec::Year(*year),
|
||||
(Some(DateArg::Month(month)), Some(DateArg::Num(year)), None) => DateSpec::YearMonth((*year, *month)),
|
||||
(Some(DateArg::Num(year)), Some(DateArg::Month(month)), None) => DateSpec::YearMonth((*year, *month)),
|
||||
|
||||
(Some(DateArg::Month(month)), None, Some(context)) => DateSpec::YearMonthContext((today.year(), *month, context)),
|
||||
(Some(DateArg::Month(month)), Some(DateArg::Num(year)), Some(context)) => DateSpec::YearMonthContext((*year, *month, context)),
|
||||
(Some(DateArg::Num(year)), Some(DateArg::Month(month)), Some(context)) => DateSpec::YearMonthContext((*year, *month, context)),
|
||||
_ => bail!("Combination of datespec args is invalid"),
|
||||
};
|
||||
Ok(ds)
|
||||
}
|
||||
|
||||
pub fn start(&self) -> NaiveDate {
|
||||
match self {
|
||||
DateSpec::Year(year) => NaiveDate::from_ymd(*year, 1, 1),
|
||||
DateSpec::YearMonth((year, month)) => NaiveDate::from_ymd(*year, *month, 1),
|
||||
DateSpec::YearMonthContext((year, month, context)) => {
|
||||
let mut year = *year - (*context / 12) as i32;
|
||||
let context = context % 12;
|
||||
let month = if context >= *month {
|
||||
year -= 1;
|
||||
12 - context + month
|
||||
} else {
|
||||
month - context
|
||||
};
|
||||
NaiveDate::from_ymd(year, month, 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> NaiveDate {
|
||||
match self {
|
||||
DateSpec::Year(year) => NaiveDate::from_ymd(year + 1, 1, 1),
|
||||
DateSpec::YearMonth((year, month)) => {
|
||||
let (year, month) = if *month == 12 {
|
||||
(*year + 1, 1)
|
||||
} else {
|
||||
(*year, *month + 1)
|
||||
};
|
||||
NaiveDate::from_ymd(year, month, 1)
|
||||
},
|
||||
DateSpec::YearMonthContext((year, month, _context)) => {
|
||||
let (year, month) = if *month == 12 {
|
||||
(*year + 1, 1)
|
||||
} else {
|
||||
(*year, *month + 1)
|
||||
};
|
||||
NaiveDate::from_ymd(year, month, 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_term_string(&self, ctx: &DateContext) -> String {
|
||||
match self {
|
||||
DateSpec::Year(year) => {
|
||||
let months = (1..=12)
|
||||
.map(|month| DateSpec::YearMonth((*year, month)))
|
||||
.collect::<Vec<_>>();
|
||||
chunk_months(ctx, &months)
|
||||
},
|
||||
DateSpec::YearMonth((year, month)) => {
|
||||
let mut w = String::new();
|
||||
|
||||
let start = Utc.ymd(*year, *month, 1);
|
||||
let days = days_in_month(*year, *month) as u32;
|
||||
|
||||
w.push_str(&format!("{:^21}\n", start.format("%B %Y")));
|
||||
w.push_str(" Su Mo Tu We Th Fr Sa\n");
|
||||
|
||||
let mut cur_week_day = start.weekday();
|
||||
let week_progress = cur_week_day.num_days_from_sunday() as usize;
|
||||
w.push_str(&" ".repeat(week_progress));
|
||||
|
||||
let mut week_written = week_progress * 3;
|
||||
for cur_day in 1..=days {
|
||||
let date = NaiveDate::from_ymd(*year, *month, cur_day);
|
||||
|
||||
if !ctx.is_future(&date) {
|
||||
let activity = ctx.activity_for_day(&date);
|
||||
w.push_str(activity.as_term_str());
|
||||
}
|
||||
|
||||
if ctx.is_today(&date) {
|
||||
w.push_str("\x1b[1m#");
|
||||
} else {
|
||||
w.push(' ');
|
||||
}
|
||||
w.push_str(&format!("{:2}", cur_day));
|
||||
week_written += 3;
|
||||
w.push_str("\x1b[0m");
|
||||
|
||||
// detect end of the week
|
||||
if cur_week_day == Weekday::Sat {
|
||||
if cur_day != days {
|
||||
w.push('\n');
|
||||
}
|
||||
week_written = 0;
|
||||
}
|
||||
|
||||
cur_week_day = cur_week_day.succ();
|
||||
}
|
||||
if week_written != 0 {
|
||||
w.push_str(&" ".repeat(21 - week_written));
|
||||
}
|
||||
|
||||
w
|
||||
}
|
||||
DateSpec::YearMonthContext((_year, _month, context)) => {
|
||||
let start = self.start();
|
||||
let mut year = start.year();
|
||||
let mut month = start.month();
|
||||
|
||||
let mut months = Vec::new();
|
||||
|
||||
for _ in 0..=*context {
|
||||
months.push(DateSpec::YearMonth((year, month)));
|
||||
|
||||
if month == 12 {
|
||||
year += 1;
|
||||
month = 1;
|
||||
} else {
|
||||
month += 1;
|
||||
}
|
||||
}
|
||||
|
||||
chunk_months(ctx, &months)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn context() -> DateContext {
|
||||
DateContext {
|
||||
events: HashMap::new(),
|
||||
max: 0,
|
||||
today: NaiveDate::from_ymd(2020, 05, 30),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_in_month_2020_05() {
|
||||
let days = days_in_month(2020, 05);
|
||||
assert_eq!(days, 31);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_in_month_2020_04() {
|
||||
let days = days_in_month(2020, 04);
|
||||
assert_eq!(days, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_in_month_2020_02() {
|
||||
let days = days_in_month(2020, 02);
|
||||
assert_eq!(days, 29);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_0() {
|
||||
let events = HashMap::new();
|
||||
let ctx = DateContext {
|
||||
events,
|
||||
max: 0,
|
||||
today: NaiveDate::from_ymd(2020, 06, 06),
|
||||
};
|
||||
let grade = ctx.activity_for_day(&NaiveDate::from_ymd(2020, 06, 06));
|
||||
assert_eq!(grade, ActivityGrade::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datespec_year_month() {
|
||||
let ds = DateSpec::YearMonth((2020, 05));
|
||||
let out = ds.to_term_string(&context());
|
||||
assert_eq!(out, " May 2020
|
||||
Su Mo Tu We Th Fr Sa
|
||||
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m\u{1b}[1m#30\u{1b}[0m
|
||||
31\u{1b}[0m ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datespec_year_month_ends_on_sat() {
|
||||
let ds = DateSpec::YearMonth((2020, 10));
|
||||
let out = ds.to_term_string(&context());
|
||||
assert_eq!(out, " October 2020
|
||||
Su Mo Tu We Th Fr Sa
|
||||
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m
|
||||
4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m
|
||||
11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m
|
||||
18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m
|
||||
25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datespec_year() {
|
||||
let ds = DateSpec::Year(2020);
|
||||
let out = ds.to_term_string(&context());
|
||||
assert_eq!(out, " January 2020 February 2020 March 2020
|
||||
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
|
||||
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 31\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 31\u{1b}[0m
|
||||
|
||||
April 2020 May 2020 June 2020
|
||||
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
|
||||
\u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 1\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 2\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 3\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 4\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 5\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 6\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 7\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 8\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 9\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 10\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 11\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 12\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 13\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 14\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 15\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 16\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 17\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 18\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 19\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 20\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 21\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 22\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 23\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m
|
||||
\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 30\u{1b}[0m \u{1b}[97m\u{1b}[48;5;238m 24\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 25\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 26\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 27\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 28\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m 29\u{1b}[0m\u{1b}[97m\u{1b}[48;5;238m\u{1b}[1m#30\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m
|
||||
31\u{1b}[0m
|
||||
July 2020 August 2020 September 2020
|
||||
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
|
||||
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 1\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m
|
||||
5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m
|
||||
12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m
|
||||
19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m
|
||||
26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m
|
||||
30\u{1b}[0m 31\u{1b}[0m
|
||||
October 2020 November 2020 December 2020
|
||||
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
|
||||
1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 1\u{1b}[0m 2\u{1b}[0m 3\u{1b}[0m 4\u{1b}[0m 5\u{1b}[0m
|
||||
4\u{1b}[0m 5\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 6\u{1b}[0m 7\u{1b}[0m 8\u{1b}[0m 9\u{1b}[0m 10\u{1b}[0m 11\u{1b}[0m 12\u{1b}[0m
|
||||
11\u{1b}[0m 12\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 13\u{1b}[0m 14\u{1b}[0m 15\u{1b}[0m 16\u{1b}[0m 17\u{1b}[0m 18\u{1b}[0m 19\u{1b}[0m
|
||||
18\u{1b}[0m 19\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 20\u{1b}[0m 21\u{1b}[0m 22\u{1b}[0m 23\u{1b}[0m 24\u{1b}[0m 25\u{1b}[0m 26\u{1b}[0m
|
||||
25\u{1b}[0m 26\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 27\u{1b}[0m 28\u{1b}[0m 29\u{1b}[0m 30\u{1b}[0m 31\u{1b}[0m
|
||||
");
|
||||
}
|
||||
}
|
||||
166
src/cal/mod.rs
Normal file
166
src/cal/mod.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use crate::errors::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod date;
|
||||
pub mod time;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DateArg {
|
||||
Month(u32),
|
||||
Num(i32),
|
||||
}
|
||||
|
||||
impl FromStr for DateArg {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<DateArg> {
|
||||
let ds = match s.to_lowercase().as_str() {
|
||||
"jan" | "january" => DateArg::Month(1),
|
||||
"feb" | "february" => DateArg::Month(2),
|
||||
"mar" | "march" => DateArg::Month(3),
|
||||
"apr" | "april" => DateArg::Month(4),
|
||||
"may" => DateArg::Month(5),
|
||||
"jun" | "june" => DateArg::Month(6),
|
||||
"jul" | "july" => DateArg::Month(7),
|
||||
"aug" | "august" => DateArg::Month(8),
|
||||
"sep" | "september" => DateArg::Month(9),
|
||||
"oct" | "october" => DateArg::Month(10),
|
||||
"nov" | "november" => DateArg::Month(11),
|
||||
"dec" | "december" => DateArg::Month(12),
|
||||
_ => {
|
||||
let num = s.parse::<i32>()
|
||||
.context("Input is not a month and not a number")?;
|
||||
DateArg::Num(num)
|
||||
},
|
||||
};
|
||||
Ok(ds)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ActivityGrade {
|
||||
None,
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
impl ActivityGrade {
|
||||
fn as_term_str(&self) -> &'static str {
|
||||
match self {
|
||||
ActivityGrade::None => "\x1b[97m\x1b[48;5;238m",
|
||||
ActivityGrade::One => "\x1b[30m\x1b[48;5;148m",
|
||||
ActivityGrade::Two => "\x1b[30m\x1b[48;5;71m",
|
||||
ActivityGrade::Three => "\x1b[97m\x1b[48;5;34m",
|
||||
ActivityGrade::Four => "\x1b[97m\x1b[48;5;22m",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ratio(num: u64, max: u64) -> ActivityGrade {
|
||||
let max = max as f64;
|
||||
let num = num as f64;
|
||||
let step = max / 4.0;
|
||||
|
||||
let x = num / step;
|
||||
|
||||
if x <= 1.0 {
|
||||
ActivityGrade::One
|
||||
} else if x <= 2.0 {
|
||||
ActivityGrade::Two
|
||||
} else if x <= 3.0 {
|
||||
ActivityGrade::Three
|
||||
} else {
|
||||
ActivityGrade::Four
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_1() {
|
||||
let grade = ActivityGrade::from_ratio(1, 1);
|
||||
assert_eq!(grade, ActivityGrade::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_2() {
|
||||
let grade = ActivityGrade::from_ratio(2, 2);
|
||||
assert_eq!(grade, ActivityGrade::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_3() {
|
||||
let grade = ActivityGrade::from_ratio(3, 3);
|
||||
assert_eq!(grade, ActivityGrade::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_4() {
|
||||
let grade = ActivityGrade::from_ratio(4, 4);
|
||||
assert_eq!(grade, ActivityGrade::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_2_but_is_1() {
|
||||
let grade = ActivityGrade::from_ratio(1, 2);
|
||||
assert_eq!(grade, ActivityGrade::Two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_3_but_is_2() {
|
||||
let grade = ActivityGrade::from_ratio(2, 3);
|
||||
assert_eq!(grade, ActivityGrade::Three);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_3_but_is_1() {
|
||||
let grade = ActivityGrade::from_ratio(1, 3);
|
||||
assert_eq!(grade, ActivityGrade::Two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_4_but_is_3() {
|
||||
let grade = ActivityGrade::from_ratio(3, 4);
|
||||
assert_eq!(grade, ActivityGrade::Three);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_4_but_is_2() {
|
||||
let grade = ActivityGrade::from_ratio(2, 4);
|
||||
assert_eq!(grade, ActivityGrade::Two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_4_but_is_1() {
|
||||
let grade = ActivityGrade::from_ratio(1, 4);
|
||||
assert_eq!(grade, ActivityGrade::One);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_5_but_is_4() {
|
||||
let grade = ActivityGrade::from_ratio(4, 5);
|
||||
assert_eq!(grade, ActivityGrade::Four);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_5_but_is_3() {
|
||||
let grade = ActivityGrade::from_ratio(3, 5);
|
||||
assert_eq!(grade, ActivityGrade::Three);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_5_but_is_2() {
|
||||
let grade = ActivityGrade::from_ratio(2, 5);
|
||||
assert_eq!(grade, ActivityGrade::Two);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_max_activity_5_but_is_1() {
|
||||
let grade = ActivityGrade::from_ratio(1, 5);
|
||||
assert_eq!(grade, ActivityGrade::One);
|
||||
}
|
||||
}
|
||||
194
src/cal/time.rs
Normal file
194
src/cal/time.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use chrono::Duration;
|
||||
use chrono::prelude::*;
|
||||
use crate::cal::{ActivityGrade, DateArg};
|
||||
use crate::models::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
const MIN_PER_DAY: u32 = 1440;
|
||||
|
||||
fn round_to_slice(time: &NaiveDateTime, slice_duration: u32) -> NaiveDateTime {
|
||||
let date = time.date();
|
||||
let hour = time.hour();
|
||||
let mins = time.minute();
|
||||
let slice = mins - (mins % slice_duration);
|
||||
date.and_hms(hour, slice, 0)
|
||||
}
|
||||
|
||||
fn setup_graph_map(events: &[Activity], slice_duration: u32) -> (HashMap<NaiveDateTime, u64>, u64) {
|
||||
debug!("Found {} events in selected range", events.len());
|
||||
|
||||
let mut cur = None;
|
||||
let mut ctr = 0;
|
||||
let mut max = 0;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
for event in events {
|
||||
let time = round_to_slice(&event.time, slice_duration);
|
||||
|
||||
if let Some(cur) = cur.as_mut() {
|
||||
if time == *cur {
|
||||
ctr += 1;
|
||||
} else {
|
||||
if ctr > max {
|
||||
max = ctr;
|
||||
}
|
||||
map.insert(cur.clone(), ctr);
|
||||
*cur = time;
|
||||
ctr = 1;
|
||||
}
|
||||
} else {
|
||||
cur = Some(time);
|
||||
ctr = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ctr > 0 {
|
||||
if let Some(cur) = cur.take() {
|
||||
if ctr > max {
|
||||
max = ctr;
|
||||
}
|
||||
map.insert(cur, ctr);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Maximum events per slice is {}", max);
|
||||
|
||||
(map, max)
|
||||
}
|
||||
|
||||
pub struct DateTimeContext {
|
||||
events: HashMap<NaiveDateTime, u64>,
|
||||
max: u64,
|
||||
now: NaiveDateTime,
|
||||
pub slice_width: u32,
|
||||
pub slice_duration: u32,
|
||||
}
|
||||
|
||||
impl DateTimeContext {
|
||||
pub fn new(events: &[Activity], now: NaiveDateTime, slice_width: u32, slice_duration: u32) -> DateTimeContext {
|
||||
let (events, max) = setup_graph_map(&events, slice_duration);
|
||||
DateTimeContext {
|
||||
events,
|
||||
max,
|
||||
now: round_to_slice(&now, slice_duration),
|
||||
slice_width,
|
||||
slice_duration,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_future(&self, time: &NaiveDateTime) -> bool {
|
||||
self.now < *time
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn slices_per_hour(&self) -> u32 {
|
||||
60 / self.slice_duration
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn hour_width(&self) -> u32 {
|
||||
self.slices_per_hour() * self.slice_width
|
||||
}
|
||||
|
||||
fn activity_for_slice(&self, time: &NaiveDateTime) -> ActivityGrade {
|
||||
if let Some(events) = self.events.get(time) {
|
||||
ActivityGrade::from_ratio(*events, self.max)
|
||||
} else {
|
||||
ActivityGrade::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DateTimeSpec {
|
||||
start: NaiveDate,
|
||||
end: NaiveDate,
|
||||
}
|
||||
|
||||
impl DateTimeSpec {
|
||||
pub fn from_args(args: &[DateArg], context: Option<u32>) -> Result<DateTimeSpec> {
|
||||
let today = Utc::today().naive_utc();
|
||||
if args.is_empty() {
|
||||
let mut start = today;
|
||||
|
||||
if let Some(context) = context {
|
||||
start = start.checked_sub_signed(Duration::days(context as i64))
|
||||
.ok_or_else(|| format_err!("Failed travel back in time"))?;
|
||||
}
|
||||
|
||||
Ok(DateTimeSpec {
|
||||
start,
|
||||
end: today,
|
||||
})
|
||||
} else {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &NaiveDate {
|
||||
&self.start
|
||||
}
|
||||
|
||||
pub fn end(&self) -> &NaiveDate {
|
||||
&self.end
|
||||
}
|
||||
|
||||
pub fn to_term_string(&self, ctx: &DateTimeContext) -> String {
|
||||
let mut w = String::new();
|
||||
|
||||
// add legend
|
||||
w.push_str(&" ".repeat(11));
|
||||
for x in 0..24 {
|
||||
w.push_str(&format!("{:02}", x));
|
||||
|
||||
for i in 0..ctx.hour_width() {
|
||||
if i >= 2 {
|
||||
w.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
w.push('\n');
|
||||
|
||||
// add days
|
||||
let mut date = self.start;
|
||||
while date <= self.end {
|
||||
w.push_str(&date.format("%Y-%m-%d ").to_string());
|
||||
|
||||
let mut hours = 0;
|
||||
let mut mins = 0;
|
||||
|
||||
for _ in 0..(MIN_PER_DAY / ctx.slice_duration) {
|
||||
let time = date.and_hms(hours, mins, 0);
|
||||
|
||||
if !ctx.is_future(&time) {
|
||||
let activity = ctx.activity_for_slice(&time);
|
||||
w.push_str(activity.as_term_str());
|
||||
} else {
|
||||
w.push_str("\x1b[0m");
|
||||
}
|
||||
|
||||
for _ in 0..ctx.slice_width {
|
||||
w.push(' ');
|
||||
}
|
||||
|
||||
mins += ctx.slice_duration;
|
||||
if mins >= 60 {
|
||||
hours += 1;
|
||||
mins = 0;
|
||||
}
|
||||
}
|
||||
w.push_str("\x1b[0m");
|
||||
|
||||
if date < self.end {
|
||||
w.push('\n');
|
||||
}
|
||||
|
||||
date = date.checked_add_signed(Duration::days(1))
|
||||
.expect("Failed to get next day");
|
||||
}
|
||||
|
||||
w
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use crate::cmd::Cmd;
|
||||
use crate::shell::Shell;
|
||||
use crate::models::*;
|
||||
use chrono::{Utc, NaiveDateTime, NaiveTime, Duration};
|
||||
use regex::Regex;
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::str::FromStr;
|
||||
@@ -15,17 +16,36 @@ pub struct TimeSpec {
|
||||
datetime: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl FromStr for TimeSpec {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let now = Utc::now().naive_utc();
|
||||
impl TimeSpec {
|
||||
fn resolve(s: &str, now: NaiveDateTime) -> Result<Self> {
|
||||
let today = NaiveDateTime::new(now.date(), NaiveTime::from_hms(0, 0, 0));
|
||||
|
||||
let datetime = match s {
|
||||
"today" => today,
|
||||
"yesterday" => today - Duration::days(1),
|
||||
// x {second,minute,hour,day,week,month,year}s? ago
|
||||
s if s.ends_with(" ago") => {
|
||||
let re = Regex::new(r"(\d+) ?(s|seconds?|m|min|minutes?|h|hours?|d|days?|w|weeks?|months?|y|years?) ago").unwrap();
|
||||
|
||||
let caps = re.captures(s)
|
||||
.ok_or_else(|| format_err!("Couldn't parse TimeSpec"))?;
|
||||
|
||||
let n = caps.get(1).unwrap().as_str()
|
||||
.parse::<i64>()
|
||||
.context("Failed to parse number in timespec")?;
|
||||
let unit = caps.get(2).unwrap();
|
||||
|
||||
let duration = match unit.as_str() {
|
||||
"s" | "second" | "seconds" => Duration::seconds(n),
|
||||
"m" | "min" | "minute" | "minutes" => Duration::minutes(n),
|
||||
"h" | "hour" | "hours" => Duration::hours(n),
|
||||
"d" | "day" | "days" => Duration::days(n),
|
||||
"w" | "week" | "weeks" => Duration::days(n * 7),
|
||||
"month" | "months" => Duration::days(n * 31),
|
||||
"y" | "year" | "years" => Duration::days(n * 365),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
now - duration
|
||||
},
|
||||
s => NaiveDateTime::from_str(s)?,
|
||||
};
|
||||
|
||||
@@ -35,6 +55,15 @@ impl FromStr for TimeSpec {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TimeSpec {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let now = Utc::now().naive_utc();
|
||||
Self::resolve(s, now)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
|
||||
pub struct Args {
|
||||
@@ -86,3 +115,63 @@ impl Cmd for Args {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn datetime() -> NaiveDateTime {
|
||||
let date = chrono::NaiveDate::from_ymd(2020, 3, 14);
|
||||
let time = chrono::NaiveTime::from_hms(16, 20, 23);
|
||||
NaiveDateTime::new(date, time)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_today() {
|
||||
let x = TimeSpec::resolve("today", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-14T00:00:00").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yesterday() {
|
||||
let x = TimeSpec::resolve("yesterday", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-13T00:00:00").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_20_min_ago() {
|
||||
let x = TimeSpec::resolve("20min ago", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-14T16:00:23").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_3_days_ago() {
|
||||
let x = TimeSpec::resolve("3 days ago", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-11T16:20:23").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_1_week_ago() {
|
||||
let x = TimeSpec::resolve("1w ago", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-07T16:20:23").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_3_months_ago() {
|
||||
let x = TimeSpec::resolve("3 months ago", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2019-12-12T16:20:23").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_1_year_ago() {
|
||||
let x = TimeSpec::resolve("1 year ago", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2019-03-15T16:20:23").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exact_time() {
|
||||
let x = TimeSpec::resolve("2020-03-14T16:20:23", datetime()).unwrap();
|
||||
assert_eq!(x.datetime, NaiveDateTime::from_str("2020-03-14T16:20:23").unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
66
src/cmd/cal_cmd.rs
Normal file
66
src/cmd/cal_cmd.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use chrono::Utc;
|
||||
use crate::cal::DateArg;
|
||||
use crate::cal::date::{DateContext, DateSpec};
|
||||
use crate::cal::time::{DateTimeContext, DateTimeSpec};
|
||||
use crate::cmd::Cmd;
|
||||
use crate::models::*;
|
||||
use crate::shell::Shell;
|
||||
use structopt::StructOpt;
|
||||
use structopt::clap::AppSettings;
|
||||
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
|
||||
pub struct Args {
|
||||
/// Show additional months for context
|
||||
#[structopt(short="C", long)]
|
||||
context: Option<u32>,
|
||||
/// Group events in 12 min slices
|
||||
#[structopt(short="T", long, group = "view")]
|
||||
time: bool,
|
||||
/// Group events by hour
|
||||
#[structopt(short="H", long, group = "view")]
|
||||
hourly: bool,
|
||||
args: Vec<DateArg>,
|
||||
}
|
||||
|
||||
impl Cmd for Args {
|
||||
#[inline]
|
||||
fn run(self, rl: &mut Shell) -> Result<()> {
|
||||
if self.time || self.hourly {
|
||||
let dts = DateTimeSpec::from_args(&self.args, self.context)
|
||||
.context("Failed to parse date spec")?;
|
||||
let filter = ActivityFilter {
|
||||
topic: None,
|
||||
since: Some(dts.start().and_hms(0, 0, 0)),
|
||||
until: Some(dts.end().and_hms(23, 59, 59)),
|
||||
location: false,
|
||||
};
|
||||
let events = Activity::query(rl.db(), &filter)?;
|
||||
|
||||
let (slice_width, slice_duration) = if self.hourly {
|
||||
(3, 60)
|
||||
} else {
|
||||
(1, 12)
|
||||
};
|
||||
|
||||
let ctx = DateTimeContext::new(&events, Utc::now().naive_utc(), slice_width, slice_duration);
|
||||
println!("{}", dts.to_term_string(&ctx));
|
||||
} else {
|
||||
let ds = DateSpec::from_args(&self.args, self.context)
|
||||
.context("Failed to parse date spec")?;
|
||||
let filter = ActivityFilter {
|
||||
topic: None,
|
||||
since: Some(ds.start().and_hms(0, 0, 0)),
|
||||
until: Some(ds.end().and_hms(23, 59, 59)),
|
||||
location: false,
|
||||
};
|
||||
let events = Activity::query(rl.db(), &filter)?;
|
||||
let ctx = DateContext::new(&events, Utc::today().naive_utc());
|
||||
println!("{}", ds.to_term_string(&ctx));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use crate::errors::*;
|
||||
use crate::blobs::Blob;
|
||||
use crate::cmd::Cmd;
|
||||
use crate::db::ttl;
|
||||
use crate::models::*;
|
||||
use crate::shell::Shell;
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
@@ -10,7 +11,6 @@ use std::io::{self, Write};
|
||||
use structopt::StructOpt;
|
||||
use structopt::clap::AppSettings;
|
||||
use strum_macros::{EnumString, IntoStaticStr};
|
||||
use crate::models::*;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
|
||||
@@ -22,7 +22,7 @@ pub struct Args {
|
||||
|
||||
impl Cmd for Args {
|
||||
fn run(self, rl: &mut Shell) -> Result<()> {
|
||||
ttl::reap_expired(rl.db())?;
|
||||
ttl::reap_expired(rl)?;
|
||||
match self.format {
|
||||
Format::Json => export::<JsonFormat>(rl),
|
||||
Format::JsonBlobs => export::<JsonBlobsFormat>(rl),
|
||||
|
||||
@@ -16,9 +16,8 @@ pub fn run(_rl: &mut Shell, _args: &[String]) -> Result<()> {
|
||||
help("autoscope", "Manage rules to automatically add entities to scope");
|
||||
help("delete", "Delete entities from the database");
|
||||
help("keyring", "Manage saved credentials");
|
||||
help("mod", "Manage installed modules");
|
||||
help("pkg", "Manage installed modules");
|
||||
help("noscope", "Exclude entities from scope");
|
||||
help("quickstart", "Install all featured modules");
|
||||
help("run", "Run the currently selected module");
|
||||
help("scope", "Include entities in the scope again");
|
||||
help("select", "Select entities from the database");
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod activity_cmd;
|
||||
pub mod add_cmd;
|
||||
pub mod autonoscope_cmd;
|
||||
pub mod autoscope_cmd;
|
||||
pub mod cal_cmd;
|
||||
pub mod delete_cmd;
|
||||
pub mod export_cmd;
|
||||
pub mod fsck_cmd;
|
||||
@@ -29,6 +30,7 @@ pub mod use_cmd;
|
||||
pub mod select_cmd;
|
||||
pub mod keyring_cmd;
|
||||
pub mod noscope_cmd;
|
||||
pub mod notify_cmd;
|
||||
pub mod pkg_cmd;
|
||||
pub mod set_cmd;
|
||||
pub mod scope_cmd;
|
||||
|
||||
99
src/cmd/notify_cmd.rs
Normal file
99
src/cmd/notify_cmd.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use crate::cmd::Cmd;
|
||||
use crate::engine::Module;
|
||||
// use crate::models::*;
|
||||
use crate::notify::{self, Notification};
|
||||
use crate::options::{self, Opt};
|
||||
use crate::shell::Shell;
|
||||
use crate::term;
|
||||
use sn0int_std::ratelimits::Ratelimiter;
|
||||
use structopt::StructOpt;
|
||||
use structopt::clap::AppSettings;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(global_settings = &[AppSettings::ColoredHelp])]
|
||||
pub struct Args {
|
||||
#[structopt(subcommand)]
|
||||
subcommand: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum Subcommand {
|
||||
/// Manually add a notification to the outbox
|
||||
Send(SendArgs),
|
||||
/// Show the current outbox
|
||||
Outbox,
|
||||
/// Execute a module directly instead of sending a message
|
||||
Exec(ExecArgs),
|
||||
/// Try to deliver all messages in our outbox
|
||||
Deliver,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct SendArgs {
|
||||
/// Evaluate the routing rules, but do not actually send a notification
|
||||
#[structopt(short="n", long)]
|
||||
pub dry_run: bool,
|
||||
pub topic: String,
|
||||
#[structopt(flatten)]
|
||||
pub notification: Notification,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ExecArgs {
|
||||
pub module: String,
|
||||
#[structopt(short="o", long="option")]
|
||||
pub options: Vec<options::Opt>,
|
||||
#[structopt(short="v", long="verbose", parse(from_occurrences))]
|
||||
verbose: u64,
|
||||
#[structopt(flatten)]
|
||||
pub notification: Notification,
|
||||
}
|
||||
|
||||
fn print_summary(module: &Module, sent: usize, errors: usize) {
|
||||
let mut out = if sent == 1 {
|
||||
String::from("Sent 1 notification")
|
||||
} else {
|
||||
format!("Sent {} notifications", sent)
|
||||
};
|
||||
|
||||
out.push_str(&format!(" with {}", module.canonical()));
|
||||
|
||||
if errors > 0 {
|
||||
out.push_str(&format!(" ({} errors)", errors));
|
||||
}
|
||||
|
||||
term::info(&out);
|
||||
}
|
||||
|
||||
fn send(args: SendArgs, rl: &mut Shell) -> Result<()> {
|
||||
rl.signal_register().catch_ctrl();
|
||||
notify::run_router(rl, &mut term::Term, &mut Ratelimiter::new(), args.dry_run, &args.topic, &args.notification)?;
|
||||
rl.signal_register().reset_ctrlc();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exec(args: ExecArgs, rl: &mut Shell) -> Result<()> {
|
||||
let module = rl.library().get(&args.module)?.clone();
|
||||
let options = Opt::collect(&args.options);
|
||||
|
||||
rl.signal_register().catch_ctrl();
|
||||
let errors = notify::exec(rl, &module, &mut Ratelimiter::new(), options, args.verbose, &args.notification)?;
|
||||
rl.signal_register().reset_ctrlc();
|
||||
|
||||
print_summary(&module, 1, errors);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Cmd for Args {
|
||||
#[inline]
|
||||
fn run(self, rl: &mut Shell) -> Result<()> {
|
||||
match self.subcommand {
|
||||
Subcommand::Send(args) => send(args, rl),
|
||||
Subcommand::Outbox => todo!(),
|
||||
Subcommand::Exec(args) => exec(args, rl),
|
||||
Subcommand::Deliver => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use crate::args::Install;
|
||||
use crate::api::Client;
|
||||
use crate::args;
|
||||
use crate::config::Config;
|
||||
use crate::cmd::{Cmd, LiteCmd};
|
||||
use crate::engine::Library;
|
||||
use crate::registry::{self, UpdateTask, Updater};
|
||||
use crate::registry::{self, InstallTask, UpdateTask, Updater};
|
||||
use crate::shell::Shell;
|
||||
use crate::update::AutoUpdater;
|
||||
use crate::worker;
|
||||
use colored::Colorize;
|
||||
use sn0int_common::ModuleID;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use structopt::StructOpt;
|
||||
@@ -47,6 +50,9 @@ pub enum SubCommand {
|
||||
/// Uninstall a module
|
||||
#[structopt(name="uninstall")]
|
||||
Uninstall(Uninstall),
|
||||
/// Install all featured modules
|
||||
#[structopt(name="quickstart")]
|
||||
Quickstart,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
@@ -66,6 +72,9 @@ pub struct List {
|
||||
/// List outdated modules
|
||||
#[structopt(long="outdated")]
|
||||
pub outdated: bool,
|
||||
/// Filter by pattern
|
||||
#[structopt(default_value="*")]
|
||||
pub pattern: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
@@ -92,6 +101,8 @@ fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) ->
|
||||
SubCommand::List(list) => {
|
||||
let autoupdate = AutoUpdater::load()?;
|
||||
|
||||
let filter = glob::Pattern::new(&list.pattern)?;
|
||||
|
||||
for module in library.list() {
|
||||
if let Some(source) = &list.source {
|
||||
if !module.source_equals(&source) {
|
||||
@@ -100,6 +111,9 @@ fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) ->
|
||||
}
|
||||
|
||||
let canonical = module.canonical();
|
||||
if !filter.matches(&canonical) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
write!(&mut out, "{} ({})", canonical.green(),
|
||||
@@ -156,6 +170,47 @@ fn run_subcommand(subcommand: SubCommand, library: &Library, config: &Config) ->
|
||||
// trigger reload
|
||||
Ok(ModuleReload::Yes)
|
||||
},
|
||||
SubCommand::Quickstart => {
|
||||
let client = Client::new(&config)?;
|
||||
let updater = Arc::new(Updater::new(&config)?);
|
||||
let mut autoupdate = AutoUpdater::load()?;
|
||||
|
||||
let installed = library.list()
|
||||
.into_iter()
|
||||
.map(|module| module.id())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let modules = client.quickstart()?
|
||||
.into_iter()
|
||||
.filter_map(|module| {
|
||||
let id = ModuleID {
|
||||
author: module.author,
|
||||
name: module.name,
|
||||
};
|
||||
|
||||
if !installed.contains(&id) {
|
||||
info!("Queueing for install: {}", id);
|
||||
Some(InstallTask::new(Install {
|
||||
module: id,
|
||||
version: None,
|
||||
force: false,
|
||||
}, updater.clone()))
|
||||
} else {
|
||||
info!("Skipping already installed module: {}", id);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
worker::spawn_multi(modules, |name| {
|
||||
autoupdate.updated(&name);
|
||||
}, 3)?;
|
||||
|
||||
autoupdate.save()?;
|
||||
|
||||
// trigger reload
|
||||
Ok(ModuleReload::Yes)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use crate::args::Install;
|
||||
use crate::api::Client;
|
||||
use crate::registry::{InstallTask, Updater};
|
||||
use crate::cmd::Cmd;
|
||||
use crate::cmd::pkg_cmd::{ArgsInteractive as PkgArgs, SubCommand, SubCommandInteractive};
|
||||
use crate::shell::Shell;
|
||||
use crate::update::AutoUpdater;
|
||||
use crate::worker;
|
||||
use std::sync::Arc;
|
||||
use crate::term;
|
||||
use structopt::StructOpt;
|
||||
use structopt::clap::AppSettings;
|
||||
use sn0int_common::ModuleID;
|
||||
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
@@ -19,34 +15,11 @@ pub struct Args {
|
||||
|
||||
pub fn run(rl: &mut Shell, args: &[String]) -> Result<()> {
|
||||
let _args = Args::from_iter_safe(args)?;
|
||||
let config = rl.config().clone();
|
||||
|
||||
let client = Client::new(&config)?;
|
||||
let updater = Arc::new(Updater::new(&config)?);
|
||||
let mut autoupdate = AutoUpdater::load()?;
|
||||
term::warn("The \x1b[1mquickstart\x1b[0m command is deprecated, use \x1b[1mpkg quickstart\x1b[0m");
|
||||
|
||||
let modules = client.quickstart()?
|
||||
.into_iter()
|
||||
.map(|module| {
|
||||
InstallTask::new(Install {
|
||||
module: ModuleID {
|
||||
author: module.author,
|
||||
name: module.name,
|
||||
},
|
||||
version: None,
|
||||
force: false,
|
||||
}, updater.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
worker::spawn_multi(modules, |name| {
|
||||
autoupdate.updated(&name);
|
||||
}, 3)?;
|
||||
|
||||
autoupdate.save()?;
|
||||
|
||||
// trigger reload
|
||||
rl.reload_modules()?;
|
||||
|
||||
Ok(())
|
||||
let args = PkgArgs {
|
||||
subcommand: SubCommandInteractive::Base(SubCommand::Quickstart),
|
||||
};
|
||||
args.run(rl)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use chrootable_https::dns::Resolver;
|
||||
use crate::args;
|
||||
use crate::blobs::{Blob, BlobStorage};
|
||||
use crate::cmd::Cmd;
|
||||
use crate::db::{ttl, Filter};
|
||||
use crate::engine::Module;
|
||||
use crate::ipc::common::StartCommand;
|
||||
use crate::models::*;
|
||||
use crate::shell::Shell;
|
||||
use crate::keyring::KeyRing;
|
||||
@@ -14,6 +16,7 @@ use crate::worker;
|
||||
use serde::Serialize;
|
||||
use serde_json;
|
||||
use sn0int_common::metadata::Source;
|
||||
use sn0int_std::ratelimits::Ratelimiter;
|
||||
use std::collections::HashMap;
|
||||
use structopt::StructOpt;
|
||||
use structopt::clap::AppSettings;
|
||||
@@ -90,7 +93,7 @@ fn prepare_args<T: Scopable + Serialize + Model>(rl: &Shell, filter: &Filter, pa
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn prepare_keyring(keyring: &mut KeyRing, module: &Module, params: &Params) -> Result<()> {
|
||||
pub fn prepare_keyring(keyring: &mut KeyRing, module: &Module, params: &Params) -> Result<()> {
|
||||
for namespace in keyring.unauthorized_namespaces(&module) {
|
||||
let grant_access = if params.deny_keyring {
|
||||
false
|
||||
@@ -113,16 +116,10 @@ fn prepare_keyring(keyring: &mut KeyRing, module: &Module, params: &Params) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>) -> Result<()> {
|
||||
let module = rl.module()
|
||||
.map(|m| m.to_owned())
|
||||
.ok_or_else(|| format_err!("No module selected"))?;
|
||||
|
||||
prepare_keyring(rl.keyring_mut(), &module, ¶ms)?;
|
||||
|
||||
fn get_args(rl: &mut Shell, module: &Module) -> Result<Vec<(serde_json::Value, Option<String>, Vec<Blob>)>> {
|
||||
let filter = rl.scoped_targets();
|
||||
|
||||
let args = match module.source() {
|
||||
match module.source() {
|
||||
Some(Source::Domains) => prepare_args::<Domain>(rl, &filter, None),
|
||||
Some(Source::Subdomains) => prepare_args::<Subdomain>(rl, &filter, None),
|
||||
Some(Source::IpAddrs) => prepare_args::<IpAddr>(rl, &filter, None),
|
||||
@@ -137,6 +134,7 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
|
||||
Some(Source::Ports) => prepare_args::<Port>(rl, &filter, None),
|
||||
Some(Source::Netblocks) => prepare_args::<Netblock>(rl, &filter, None),
|
||||
Some(Source::CryptoAddrs(currency)) => prepare_args::<CryptoAddr>(rl, &filter, currency.as_ref()),
|
||||
Some(Source::Notifications) => bail!("Notification modules can't be executed like this"),
|
||||
Some(Source::KeyRing(namespace)) => {
|
||||
let keyring = rl.keyring();
|
||||
if keyring.is_access_granted(&module, &namespace) {
|
||||
@@ -152,10 +150,47 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
|
||||
}
|
||||
},
|
||||
None => Ok(vec![(serde_json::Value::Null, None, vec![])]),
|
||||
}?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_sandbox_init_msg(rl: &mut Shell, params: Params, options: HashMap<String, String>) -> Result<()> {
|
||||
let module = rl.module()
|
||||
.map(|m| m.to_owned())
|
||||
.ok_or_else(|| format_err!("No module selected"))?;
|
||||
|
||||
prepare_keyring(rl.keyring_mut(), &module, ¶ms)?;
|
||||
let keyring = rl.keyring().request_keys(&module);
|
||||
|
||||
let dns_config = Resolver::from_system_v4()?;
|
||||
let proxy = rl.config().network.proxy.clone();
|
||||
|
||||
let args = get_args(rl, &module)?;
|
||||
for (arg, _pretty_arg, blobs) in args {
|
||||
let start_cmd = StartCommand::new(params.verbose,
|
||||
keyring.clone(),
|
||||
dns_config.clone(),
|
||||
proxy.clone(),
|
||||
options.clone(),
|
||||
module.clone(),
|
||||
arg,
|
||||
blobs);
|
||||
let out = serde_json::to_string(&start_cmd)?;
|
||||
println!("{}", out);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>) -> Result<()> {
|
||||
let module = rl.module()
|
||||
.map(|m| m.to_owned())
|
||||
.ok_or_else(|| format_err!("No module selected"))?;
|
||||
|
||||
prepare_keyring(rl.keyring_mut(), &module, ¶ms)?;
|
||||
let args = get_args(rl, &module)?;
|
||||
|
||||
rl.signal_register().catch_ctrl();
|
||||
let errors = worker::spawn(rl, &module, args, ¶ms, rl.config().network.proxy.clone(), options);
|
||||
let errors = worker::spawn(rl, &module, &mut Ratelimiter::new(), args, ¶ms, rl.config().network.proxy.clone(), options);
|
||||
rl.signal_register().reset_ctrlc();
|
||||
|
||||
if errors > 0 {
|
||||
@@ -173,7 +208,7 @@ pub fn execute(rl: &mut Shell, params: Params, options: HashMap<String, String>)
|
||||
|
||||
impl Cmd for Args {
|
||||
fn run(self, rl: &mut Shell) -> Result<()> {
|
||||
ttl::reap_expired(rl.db())?;
|
||||
ttl::reap_expired(rl)?;
|
||||
let options = match rl.options_mut() {
|
||||
Some(options) => options.clone(),
|
||||
_ => HashMap::new(),
|
||||
|
||||
@@ -22,12 +22,17 @@ pub struct Args {
|
||||
/// Print paths to blobs
|
||||
#[structopt(long="paths", group="output")]
|
||||
paths: bool,
|
||||
/// Count rows returned
|
||||
#[structopt(short="c", long="count", group="output")]
|
||||
count: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Output {
|
||||
Normal,
|
||||
Json,
|
||||
Paths,
|
||||
Count,
|
||||
}
|
||||
|
||||
struct Printer<'a, 'b> {
|
||||
@@ -41,6 +46,8 @@ impl<'a, 'b> Printer<'a, 'b> {
|
||||
Output::Json
|
||||
} else if args.paths {
|
||||
Output::Paths
|
||||
} else if args.count {
|
||||
Output::Count
|
||||
} else {
|
||||
Output::Normal
|
||||
};
|
||||
@@ -52,25 +59,32 @@ impl<'a, 'b> Printer<'a, 'b> {
|
||||
}
|
||||
|
||||
pub fn select<T: Model + Detailed + Serialize>(&self, filter: &Filter) -> Result<()> {
|
||||
for obj in self.rl.db().filter::<T>(&filter.parse_optional()?)? {
|
||||
match self.output {
|
||||
Output::Normal => println!("{}", obj.detailed(self.rl.db())?),
|
||||
Output::Json => {
|
||||
let v = serde_json::to_string(&obj)?;
|
||||
println!("{}", v);
|
||||
},
|
||||
Output::Paths => {
|
||||
let blob = obj.blob()
|
||||
.ok_or_else(|| format_err!("This model isn't linked to blob storage"))?;
|
||||
let query = self.rl.db().filter::<T>(&filter.parse_optional()?)?;
|
||||
|
||||
let path = self.rl.blobs()
|
||||
.join(blob)?;
|
||||
if self.output == Output::Count {
|
||||
println!("{}", query.len());
|
||||
} else {
|
||||
for obj in query {
|
||||
match self.output {
|
||||
Output::Normal => println!("{}", obj.detailed(self.rl.db())?),
|
||||
Output::Json => {
|
||||
let v = serde_json::to_string(&obj)?;
|
||||
println!("{}", v);
|
||||
},
|
||||
Output::Paths => {
|
||||
let blob = obj.blob()
|
||||
.ok_or_else(|| format_err!("This model isn't linked to blob storage"))?;
|
||||
|
||||
let path = path.to_str()
|
||||
.ok_or_else(|| format_err!("Path is invalid utf-8"))?;
|
||||
let path = self.rl.blobs()
|
||||
.join(blob)?;
|
||||
|
||||
println!("{}", path);
|
||||
},
|
||||
let path = path.to_str()
|
||||
.ok_or_else(|| format_err!("Path is invalid utf-8"))?;
|
||||
|
||||
println!("{}", path);
|
||||
},
|
||||
Output::Count => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +94,7 @@ impl<'a, 'b> Printer<'a, 'b> {
|
||||
|
||||
impl Cmd for Args {
|
||||
fn run(self, rl: &mut Shell) -> Result<()> {
|
||||
ttl::reap_expired(rl.db())?;
|
||||
ttl::reap_expired(rl)?;
|
||||
let printer = Printer::new(rl, &self);
|
||||
|
||||
match &self.subcommand {
|
||||
|
||||
@@ -44,6 +44,7 @@ pub fn run(rl: &mut Shell, args: &[String]) -> Result<()> {
|
||||
Source::Ports => select::<Port>(rl, None)?,
|
||||
Source::Netblocks => select::<Netblock>(rl, None)?,
|
||||
Source::CryptoAddrs(currency) => select::<CryptoAddr>(rl, currency.as_ref())?,
|
||||
Source::Notifications => bail!("Notifications can't be set as target"),
|
||||
Source::KeyRing(namespace) => {
|
||||
for key in rl.keyring().list_for(&namespace) {
|
||||
println!("{}:{}", key.namespace, key.name);
|
||||
@@ -79,6 +80,7 @@ fn count_selected(rl: &mut Shell, source: &Source) -> Result<usize> {
|
||||
Source::Ports => db.filter::<Port>(&filter)?.len(),
|
||||
Source::Netblocks => db.filter::<Netblock>(&filter)?.len(),
|
||||
Source::CryptoAddrs(currency) => db.filter_with_param::<CryptoAddr>(&filter, currency.as_ref())?.len(),
|
||||
Source::Notifications => bail!("Notifications can't be set as target"),
|
||||
Source::KeyRing(namespace) => rl.keyring().list_for(&namespace).len(),
|
||||
};
|
||||
Ok(num)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use dirs;
|
||||
use crate::errors::*;
|
||||
use crate::notify::NotificationConfig;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -15,6 +16,8 @@ pub struct Config {
|
||||
pub namespaces: HashMap<String, PathBuf>,
|
||||
#[serde(default)]
|
||||
pub network: NetworkConfig,
|
||||
#[serde(default)]
|
||||
pub notifications: HashMap<String, NotificationConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -304,7 +304,7 @@ impl Database {
|
||||
Ok(ipaddr_update.id)
|
||||
}
|
||||
|
||||
pub fn update_url(&self, url_update: &UrlUpdate) -> Result<i32> {
|
||||
pub fn update_url(&self, url_update: &UrlChangeset) -> Result<i32> {
|
||||
use crate::schema::urls::columns::*;
|
||||
diesel::update(urls::table.filter(id.eq(url_update.id)))
|
||||
.set(url_update)
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::errors::*;
|
||||
use crate::db::{Database, Table};
|
||||
use crate::schema::*;
|
||||
use crate::errors::*;
|
||||
use crate::models::*;
|
||||
use crate::notify::{self, Notification};
|
||||
use crate::schema::*;
|
||||
use crate::shell::Shell;
|
||||
use crate::term::{self, Term};
|
||||
use chrono::{NaiveDateTime, Duration, Utc};
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use sn0int_std::ratelimits::Ratelimiter;
|
||||
|
||||
|
||||
#[derive(Identifiable, Queryable, AsChangeset, PartialEq, Debug)]
|
||||
@@ -13,6 +17,7 @@ pub struct Ttl {
|
||||
pub id: i32,
|
||||
pub family: String,
|
||||
pub key: i32,
|
||||
pub value: String,
|
||||
pub expire: NaiveDateTime,
|
||||
}
|
||||
|
||||
@@ -21,14 +26,16 @@ pub struct Ttl {
|
||||
pub struct NewTtl<'a> {
|
||||
pub family: &'a str,
|
||||
pub key: i32,
|
||||
pub value: String,
|
||||
pub expire: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Ttl {
|
||||
pub fn new(obj: &Insert, key: i32, expire: NaiveDateTime) -> NewTtl {
|
||||
pub fn new(obj: &Insert, key: i32, value: String, expire: NaiveDateTime) -> NewTtl {
|
||||
NewTtl {
|
||||
family: obj.table(),
|
||||
key,
|
||||
value,
|
||||
expire,
|
||||
}
|
||||
}
|
||||
@@ -59,7 +66,7 @@ impl Ttl {
|
||||
expire_at.naive_utc()
|
||||
}
|
||||
|
||||
pub fn create(obj: &Insert, key: i32, ttl: i32, db: &Database) -> Result<()> {
|
||||
pub fn create(obj: &Insert, key: i32, value: String, ttl: i32, db: &Database) -> Result<()> {
|
||||
debug!("Creating ttl on record");
|
||||
let expire = Self::ttl_to_datetime(ttl);
|
||||
|
||||
@@ -67,6 +74,7 @@ impl Ttl {
|
||||
.values(NewTtl {
|
||||
family: obj.table(),
|
||||
key,
|
||||
value,
|
||||
expire,
|
||||
})
|
||||
.execute(db.db())?;
|
||||
@@ -127,12 +135,22 @@ impl Ttl {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reap_expired(db: &Database) -> Result<()> {
|
||||
pub fn reap_expired(rl: &mut Shell) -> Result<()> {
|
||||
debug!("Reaping expired entities");
|
||||
|
||||
for expired in Ttl::expired(db)? {
|
||||
let mut ratelimit = Ratelimiter::new();
|
||||
for expired in Ttl::expired(rl.db())? {
|
||||
debug!("Expired: {:?}", expired);
|
||||
expired.delete(db)?;
|
||||
expired.delete(rl.db())?;
|
||||
|
||||
let subject = format!("Deleted {} {:?}", &expired.family, &expired.value);
|
||||
let topic = &format!("db:{}:{}:delete", &expired.family, &expired.value);
|
||||
if let Err(err) = notify::trigger_notify_event(rl, &mut Term, &mut ratelimit, &topic, &Notification {
|
||||
subject,
|
||||
body: None,
|
||||
}) {
|
||||
term::error(&format!("Failed to send notifications: {}", err));
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Finished reaping expired entities");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use crate::db::Family;
|
||||
use crate::engine::{Environment, Reporter};
|
||||
use crate::engine::{Environment, IpcChild};
|
||||
use crate::geoip::{MaxmindReader, GeoIP, AsnDB};
|
||||
use crate::hlua::{self, AnyLuaValue};
|
||||
use crate::keyring::KeyRingEntry;
|
||||
@@ -17,6 +17,7 @@ use crate::ratelimits::RatelimitResponse;
|
||||
use chrootable_https::{self, Resolver};
|
||||
use serde_json;
|
||||
use sn0int_std::blobs::{Blob, BlobState};
|
||||
use sn0int_std::mqtt::{MqttClient, MqttOptions};
|
||||
use sn0int_std::web::WebState;
|
||||
use std::collections::HashMap;
|
||||
use std::result;
|
||||
@@ -113,8 +114,8 @@ pub trait State {
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
fn db_update(&self, object: String, update: Update) -> Result<DatabaseResponse> {
|
||||
self.send(&Event::Database(DatabaseEvent::Update((object, update))));
|
||||
fn db_update(&self, family: Family, value: String, update: Update) -> Result<DatabaseResponse> {
|
||||
self.send(&Event::Database(DatabaseEvent::Update((family, value, update))));
|
||||
self.db_recv()
|
||||
.context("Failed to update database")
|
||||
.map_err(Error::from)
|
||||
@@ -178,6 +179,10 @@ pub trait State {
|
||||
|
||||
fn get_ws(&self, id: &str)-> Arc<Mutex<WebSocket>>;
|
||||
|
||||
fn mqtt_connect(&self, url: url::Url, options: &MqttOptions) -> Result<String>;
|
||||
|
||||
fn get_mqtt(&self, id: &str)-> Arc<Mutex<MqttClient>>;
|
||||
|
||||
fn http_mksession(&self) -> String;
|
||||
|
||||
fn http_request(&self, session_id: &str, method: String, url: String, options: RequestOptions) -> HttpRequest;
|
||||
@@ -196,9 +201,10 @@ pub trait State {
|
||||
// #[derive(Debug)]
|
||||
pub struct LuaState {
|
||||
error: Mutex<Option<Error>>,
|
||||
logger: Arc<Mutex<Box<dyn Reporter>>>,
|
||||
logger: Arc<Mutex<Box<dyn IpcChild>>>,
|
||||
socket_sessions: Mutex<HashMap<String, Arc<Mutex<Socket>>>>,
|
||||
ws_sessions: Mutex<HashMap<String, Arc<Mutex<WebSocket>>>>,
|
||||
mqtt_sessions: Mutex<HashMap<String, Arc<Mutex<MqttClient>>>>,
|
||||
blobs: Mutex<HashMap<String, Arc<Blob>>>,
|
||||
http_sessions: Mutex<HashMap<String, HttpSession>>,
|
||||
http_clients: Mutex<HashMap<String, Arc<chrootable_https::Client<Resolver>>>>,
|
||||
@@ -340,6 +346,22 @@ impl State for LuaState {
|
||||
sock.clone()
|
||||
}
|
||||
|
||||
fn mqtt_connect(&self, url: url::Url, options: &MqttOptions) -> Result<String> {
|
||||
let mut mtx = self.mqtt_sessions.lock().unwrap();
|
||||
let id = self.random_id();
|
||||
|
||||
let sock = MqttClient::connect(&self.dns_config, url, options)?;
|
||||
mtx.insert(id.clone(), Arc::new(Mutex::new(sock)));
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn get_mqtt(&self, id: &str)-> Arc<Mutex<MqttClient>> {
|
||||
let mtx = self.mqtt_sessions.lock().unwrap();
|
||||
let sock = mtx.get(id).expect("Invalid mqtt reference"); // TODO
|
||||
sock.clone()
|
||||
}
|
||||
|
||||
fn http_mksession(&self) -> String {
|
||||
let mut mtx = self.http_sessions.lock().unwrap();
|
||||
let (id, session) = HttpSession::new();
|
||||
@@ -426,7 +448,7 @@ pub struct Script {
|
||||
code: String,
|
||||
}
|
||||
|
||||
pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn Reporter>>>) -> (hlua::Lua<'a>, Arc<LuaState>) {
|
||||
pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn IpcChild>>>) -> (hlua::Lua<'a>, Arc<LuaState>) {
|
||||
debug!("Creating lua context");
|
||||
let mut lua = hlua::Lua::new();
|
||||
lua.open_string();
|
||||
@@ -439,6 +461,7 @@ pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn Reporter>>>) -> (hlua
|
||||
logger,
|
||||
socket_sessions: Mutex::new(HashMap::new()),
|
||||
ws_sessions: Mutex::new(HashMap::new()),
|
||||
mqtt_sessions: Mutex::new(HashMap::new()),
|
||||
blobs: Mutex::new(HashMap::new()),
|
||||
http_sessions: Mutex::new(HashMap::new()),
|
||||
http_clients: Mutex::new(HashMap::new()),
|
||||
@@ -501,9 +524,14 @@ pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn Reporter>>>) -> (hlua
|
||||
runtime::json_decode(&mut lua, state.clone());
|
||||
runtime::json_decode_stream(&mut lua, state.clone());
|
||||
runtime::json_encode(&mut lua, state.clone());
|
||||
runtime::key_trunc_pad(&mut lua, state.clone());
|
||||
runtime::keyring(&mut lua, state.clone());
|
||||
runtime::last_err(&mut lua, state.clone());
|
||||
runtime::md5(&mut lua, state.clone());
|
||||
runtime::mqtt_connect(&mut lua, state.clone());
|
||||
runtime::mqtt_subscribe(&mut lua, state.clone());
|
||||
runtime::mqtt_recv(&mut lua, state.clone());
|
||||
runtime::mqtt_ping(&mut lua, state.clone());
|
||||
runtime::pgp_pubkey(&mut lua, state.clone());
|
||||
runtime::pgp_pubkey_armored(&mut lua, state.clone());
|
||||
runtime::print(&mut lua, state.clone());
|
||||
@@ -536,6 +564,7 @@ pub fn ctx<'a>(env: Environment, logger: Arc<Mutex<Box<dyn Reporter>>>) -> (hlua
|
||||
runtime::sock_recvuntil(&mut lua, state.clone());
|
||||
runtime::sock_sendafter(&mut lua, state.clone());
|
||||
runtime::sock_newline(&mut lua, state.clone());
|
||||
runtime::sodium_secretbox_open(&mut lua, state.clone());
|
||||
runtime::status(&mut lua, state.clone());
|
||||
runtime::stdin_read_line(&mut lua, state.clone());
|
||||
runtime::stdin_read_to_end(&mut lua, state.clone());
|
||||
@@ -592,7 +621,7 @@ impl Script {
|
||||
}
|
||||
|
||||
pub fn run(&self, env: Environment,
|
||||
tx: Arc<Mutex<Box<dyn Reporter>>>,
|
||||
tx: Arc<Mutex<Box<dyn IpcChild>>>,
|
||||
arg: AnyLuaValue,
|
||||
) -> Result<()> {
|
||||
let (mut lua, state) = ctx(env, tx);
|
||||
@@ -623,11 +652,11 @@ impl Script {
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test(&self) -> Result<()> {
|
||||
use crate::engine::DummyReporter;
|
||||
use crate::ipc::child::DummyIpcChild;
|
||||
use crate::geoip::Maxmind;
|
||||
use crate::paths;
|
||||
let keyring = Vec::new();
|
||||
let dns_config = Resolver::from_system()?;
|
||||
let dns_config = Resolver::from_system_v4()?;
|
||||
let proxy = None;
|
||||
let psl = PslReader::String(r#"
|
||||
// ===BEGIN ICANN DOMAINS===
|
||||
@@ -652,6 +681,6 @@ a.prod.fastly.net
|
||||
geoip,
|
||||
asn,
|
||||
};
|
||||
self.run(env, DummyReporter::new(), AnyLuaValue::LuaNil)
|
||||
self.run(env, DummyIpcChild::new(), AnyLuaValue::LuaNil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
use crate::errors::*;
|
||||
use chrootable_https::dns::Resolver;
|
||||
use crate::blobs::Blob;
|
||||
use crate::engine::{Environment, Module, Reporter};
|
||||
use crate::geoip::MaxmindReader;
|
||||
use crate::keyring::KeyRingEntry;
|
||||
use crate::psl::PslReader;
|
||||
use serde_json;
|
||||
use crate::worker::{Event, Event2, LogEvent, ExitEvent, EventSender, EventWithCallback};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, BufReader, BufRead, stdin, Stdin, Stdout};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::process::{Command, Child, Stdio, ChildStdin, ChildStdout};
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StartCommand {
|
||||
verbose: u64,
|
||||
keyring: Vec<KeyRingEntry>,
|
||||
dns_config: Resolver,
|
||||
proxy: Option<SocketAddr>,
|
||||
options: HashMap<String, String>,
|
||||
module: Module,
|
||||
arg: serde_json::Value,
|
||||
blobs: Vec<Blob>,
|
||||
}
|
||||
|
||||
impl StartCommand {
|
||||
pub fn new(verbose: u64,
|
||||
keyring: Vec<KeyRingEntry>,
|
||||
dns_config: Resolver,
|
||||
proxy: Option<SocketAddr>,
|
||||
options: HashMap<String, String>,
|
||||
module: Module,
|
||||
arg: serde_json::Value,
|
||||
blobs: Vec<Blob>,
|
||||
) -> StartCommand {
|
||||
StartCommand {
|
||||
verbose,
|
||||
keyring,
|
||||
dns_config,
|
||||
proxy,
|
||||
options,
|
||||
module,
|
||||
arg,
|
||||
blobs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Supervisor {
|
||||
child: Child,
|
||||
stdin: ChildStdin,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
}
|
||||
|
||||
impl Supervisor {
|
||||
pub fn setup(module: &Module) -> Result<Supervisor> {
|
||||
let exe = match env::current_exe() {
|
||||
Ok(exe) => exe.into_os_string(),
|
||||
_ => OsString::from("sn0int"),
|
||||
};
|
||||
|
||||
let mut child = Command::new(exe)
|
||||
.arg("sandbox")
|
||||
.arg(&module.canonical())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn child process")?;
|
||||
|
||||
let stdin = child.stdin.take().expect("Failed to take child stdin");
|
||||
let stdout = child.stdout.take().expect("Failed to take child stdout");
|
||||
let stdout = BufReader::new(stdout);
|
||||
|
||||
Ok(Supervisor {
|
||||
child,
|
||||
stdin,
|
||||
stdout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_start(&mut self, start: &StartCommand) -> Result<()> {
|
||||
let start = serde_json::to_value(&start)?;
|
||||
self.send(&start)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send(&mut self, value: &serde_json::Value) -> Result<()> {
|
||||
let mut value = serde_json::to_string(value)?;
|
||||
value.push('\n');
|
||||
self.stdin.write_all(value.as_bytes())?;
|
||||
debug!("Supervisor sent: {:?}", value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_struct<T: serde::Serialize>(&mut self, value: T, tx: &EventSender) {
|
||||
let value = serde_json::to_value(value).expect("Failed to serialize reply");
|
||||
if let Err(_) = self.send(&value) {
|
||||
tx.send(Event2::Log(LogEvent::Error("Failed to send to child".into())));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv(&mut self) -> Result<Event> {
|
||||
let mut line = String::new();
|
||||
let len = self.stdout.read_line(&mut line)?;
|
||||
|
||||
let event = serde_json::from_str(&line[..len])?;
|
||||
debug!("Supervisor received: {:?}", event);
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub fn wait(&mut self) -> Result<()> {
|
||||
let exit = self.child.wait()
|
||||
.context("Failed to wait for child")?;
|
||||
|
||||
if exit.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Child signaled error")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_event_callback<T: EventWithCallback>(&mut self, event: T, tx: &EventSender)
|
||||
where <T as EventWithCallback>::Payload: serde::Serialize
|
||||
{
|
||||
let (tx2, rx2) = mpsc::channel();
|
||||
tx.send(event.with_callback(tx2));
|
||||
let reply = rx2.recv().unwrap();
|
||||
|
||||
self.send_struct(reply, tx);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StdioReporter {
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
}
|
||||
|
||||
impl StdioReporter {
|
||||
pub fn setup() -> StdioReporter {
|
||||
let stdin = io::stdin();
|
||||
let stdout = io::stdout();
|
||||
|
||||
StdioReporter {
|
||||
stdin,
|
||||
stdout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_start(&mut self) -> Result<StartCommand> {
|
||||
let value = self.recv()?;
|
||||
let event = serde_json::from_value(value)?;
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reporter for StdioReporter {
|
||||
fn send(&mut self, event: &Event) -> Result<()> {
|
||||
let mut event = serde_json::to_string(&event)?;
|
||||
event.push('\n');
|
||||
self.stdout.write_all(event.as_bytes())?;
|
||||
debug!("Reporter sent: {:?}", event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value> {
|
||||
let mut line = String::new();
|
||||
let len = self.stdin.read_line(&mut line)?;
|
||||
|
||||
let event = serde_json::from_str(&line[..len])?;
|
||||
debug!("Reporter received: {:?}", event);
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_module(module: Module,
|
||||
tx: &EventSender,
|
||||
arg: serde_json::Value,
|
||||
keyring: Vec<KeyRingEntry>,
|
||||
verbose: u64,
|
||||
has_stdin: bool,
|
||||
proxy: Option<SocketAddr>,
|
||||
options: HashMap<String, String>,
|
||||
blobs: Vec<Blob>,
|
||||
) -> Result<ExitEvent> {
|
||||
let dns_config = Resolver::from_system()?;
|
||||
|
||||
let mut reader = if has_stdin {
|
||||
Some(BufReader::new(stdin()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut supervisor = Supervisor::setup(&module)?;
|
||||
supervisor.send_start(&StartCommand::new(verbose, keyring, dns_config, proxy, options, module, arg, blobs))?;
|
||||
|
||||
let exit = loop {
|
||||
match supervisor.recv()? {
|
||||
Event::Log(event) => tx.send(Event2::Log(event)),
|
||||
Event::Database(object) => supervisor.send_event_callback(object, &tx),
|
||||
Event::Stdio(object) => object.apply(&mut supervisor, tx, &mut reader),
|
||||
Event::Ratelimit(req) => supervisor.send_event_callback(req, &tx),
|
||||
Event::Blob(blob) => supervisor.send_event_callback(blob, &tx),
|
||||
Event::Exit(event) => {
|
||||
if let ExitEvent::Err(err) = &event {
|
||||
tx.send(Event2::Log(LogEvent::Error(err.clone())));
|
||||
}
|
||||
break event;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
supervisor.wait()?;
|
||||
|
||||
Ok(exit)
|
||||
}
|
||||
|
||||
pub fn run_worker(geoip: Option<MaxmindReader>, asn: Option<MaxmindReader>, psl: PslReader) -> Result<()> {
|
||||
let mut reporter = StdioReporter::setup();
|
||||
let start = reporter.recv_start()?;
|
||||
|
||||
let environment = Environment {
|
||||
verbose: start.verbose,
|
||||
keyring: start.keyring,
|
||||
dns_config: start.dns_config,
|
||||
proxy: start.proxy,
|
||||
options: start.options,
|
||||
blobs: start.blobs,
|
||||
psl,
|
||||
geoip,
|
||||
asn,
|
||||
};
|
||||
|
||||
let mtx: Arc<Mutex<Box<dyn Reporter>>> = Arc::new(Mutex::new(Box::new(reporter)));
|
||||
let result = start.module.run(environment,
|
||||
mtx.clone(),
|
||||
start.arg.into());
|
||||
let mut reporter = Arc::try_unwrap(mtx).expect("Failed to consume Arc")
|
||||
.into_inner().expect("Failed to consume Mutex");
|
||||
|
||||
let event = result.into();
|
||||
reporter.send(&Event::Exit(event))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use crate::config::Config;
|
||||
use crate::geoip::MaxmindReader;
|
||||
use crate::json::LuaJsonValue;
|
||||
use crate::keyring::KeyRingEntry;
|
||||
use serde_json;
|
||||
use std::fs;
|
||||
use std::fmt::Debug;
|
||||
use std::path::PathBuf;
|
||||
@@ -13,6 +12,7 @@ use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::engine::ctx::Script;
|
||||
use crate::ipc::child::IpcChild;
|
||||
use sn0int_common::ModuleID;
|
||||
use sn0int_common::metadata::{Metadata, Source};
|
||||
use chrootable_https::dns::Resolver;
|
||||
@@ -21,10 +21,9 @@ use crate::paths;
|
||||
use std::cmp::Ordering;
|
||||
use std::path::Path;
|
||||
use crate::term;
|
||||
use crate::worker::{self, Event};
|
||||
use crate::worker;
|
||||
|
||||
pub mod ctx;
|
||||
pub mod isolation;
|
||||
pub use sn0int_std::engine::structs;
|
||||
|
||||
|
||||
@@ -299,9 +298,9 @@ impl Module {
|
||||
self.private_module
|
||||
}
|
||||
|
||||
pub fn run(&self, env: Environment, reporter: Arc<Mutex<Box<dyn Reporter>>>, arg: LuaJsonValue) -> Result<()> {
|
||||
pub fn run(&self, env: Environment, ipc_child: Arc<Mutex<Box<dyn IpcChild>>>, arg: LuaJsonValue) -> Result<()> {
|
||||
debug!("Executing lua script {}", self.canonical());
|
||||
self.script.run(env, reporter, arg.into())
|
||||
self.script.run(env, ipc_child, arg.into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -320,28 +319,3 @@ impl Module {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Reporter: Debug {
|
||||
fn send(&mut self, event: &Event) -> Result<()>;
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DummyReporter;
|
||||
|
||||
impl DummyReporter {
|
||||
pub fn new() -> Arc<Mutex<Box<dyn Reporter>>> {
|
||||
Arc::new(Mutex::new(Box::new(DummyReporter)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Reporter for DummyReporter {
|
||||
fn send(&mut self, _event: &Event) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value> {
|
||||
unimplemented!("DummyReporter::recv doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
110
src/ipc/child.rs
Normal file
110
src/ipc/child.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::errors::*;
|
||||
use crate::ipc::common::*;
|
||||
use crate::engine::Environment;
|
||||
use crate::geoip::MaxmindReader;
|
||||
use crate::psl::PslReader;
|
||||
use serde_json;
|
||||
use crate::worker::Event;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, Stdin, Stdout};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
||||
pub trait IpcChild: Debug {
|
||||
fn send(&mut self, event: &Event) -> Result<()>;
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StdioIpcChild {
|
||||
stdin: Stdin,
|
||||
stdout: Stdout,
|
||||
}
|
||||
|
||||
impl StdioIpcChild {
|
||||
pub fn setup() -> StdioIpcChild {
|
||||
let stdin = io::stdin();
|
||||
let stdout = io::stdout();
|
||||
|
||||
StdioIpcChild {
|
||||
stdin,
|
||||
stdout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv_start(&mut self) -> Result<StartCommand> {
|
||||
let value = self.recv()?;
|
||||
let event = serde_json::from_value(value)?;
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcChild for StdioIpcChild {
|
||||
fn send(&mut self, event: &Event) -> Result<()> {
|
||||
let mut event = serde_json::to_string(&event)?;
|
||||
event.push('\n');
|
||||
debug!("IpcChild sends: {:?}", event);
|
||||
self.stdout.write_all(event.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value> {
|
||||
let mut line = String::new();
|
||||
let len = self.stdin.read_line(&mut line)?;
|
||||
|
||||
let event = serde_json::from_str(&line[..len])?;
|
||||
debug!("IpcChild received: {:?}", event);
|
||||
Ok(event)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DummyIpcChild;
|
||||
|
||||
impl DummyIpcChild {
|
||||
pub fn new() -> Arc<Mutex<Box<dyn IpcChild>>> {
|
||||
Arc::new(Mutex::new(Box::new(DummyIpcChild)))
|
||||
}
|
||||
}
|
||||
|
||||
impl IpcChild for DummyIpcChild {
|
||||
fn send(&mut self, _event: &Event) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv(&mut self) -> Result<serde_json::Value> {
|
||||
unimplemented!("DummyIpcChild::recv doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(geoip: Option<MaxmindReader>, asn: Option<MaxmindReader>, psl: PslReader) -> Result<()> {
|
||||
let mut ipc_child = StdioIpcChild::setup();
|
||||
let start = ipc_child.recv_start()?;
|
||||
|
||||
let environment = Environment {
|
||||
verbose: start.verbose,
|
||||
keyring: start.keyring,
|
||||
dns_config: start.dns_config,
|
||||
proxy: start.proxy,
|
||||
options: start.options,
|
||||
blobs: start.blobs,
|
||||
psl,
|
||||
geoip,
|
||||
asn,
|
||||
};
|
||||
|
||||
let mtx: Arc<Mutex<Box<dyn IpcChild>>> = Arc::new(Mutex::new(Box::new(ipc_child)));
|
||||
let result = start.module.run(environment,
|
||||
mtx.clone(),
|
||||
start.arg.into());
|
||||
let mut ipc_child = Arc::try_unwrap(mtx).expect("Failed to consume Arc")
|
||||
.into_inner().expect("Failed to consume Mutex");
|
||||
|
||||
let event = result.into();
|
||||
ipc_child.send(&Event::Exit(event))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
44
src/ipc/common.rs
Normal file
44
src/ipc/common.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use chrootable_https::dns::Resolver;
|
||||
use crate::blobs::Blob;
|
||||
use crate::engine::Module;
|
||||
use crate::keyring::KeyRingEntry;
|
||||
use serde_json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StartCommand {
|
||||
pub verbose: u64,
|
||||
pub keyring: Vec<KeyRingEntry>,
|
||||
pub dns_config: Resolver,
|
||||
pub proxy: Option<SocketAddr>,
|
||||
pub options: HashMap<String, String>,
|
||||
pub module: Module,
|
||||
pub arg: serde_json::Value,
|
||||
pub blobs: Vec<Blob>,
|
||||
}
|
||||
|
||||
impl StartCommand {
|
||||
pub fn new(verbose: u64,
|
||||
keyring: Vec<KeyRingEntry>,
|
||||
dns_config: Resolver,
|
||||
proxy: Option<SocketAddr>,
|
||||
options: HashMap<String, String>,
|
||||
module: Module,
|
||||
arg: serde_json::Value,
|
||||
blobs: Vec<Blob>,
|
||||
) -> StartCommand {
|
||||
StartCommand {
|
||||
verbose,
|
||||
keyring,
|
||||
dns_config,
|
||||
proxy,
|
||||
options,
|
||||
module,
|
||||
arg,
|
||||
blobs,
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/ipc/mod.rs
Normal file
3
src/ipc/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod child;
|
||||
pub mod common;
|
||||
pub mod parent;
|
||||
144
src/ipc/parent.rs
Normal file
144
src/ipc/parent.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use crate::errors::*;
|
||||
use crate::ipc::common::*;
|
||||
use chrootable_https::dns::Resolver;
|
||||
use crate::blobs::Blob;
|
||||
use crate::engine::Module;
|
||||
use crate::keyring::KeyRingEntry;
|
||||
use serde_json;
|
||||
use crate::worker::{Event, Event2, LogEvent, ExitEvent, EventSender, EventWithCallback};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{BufReader, BufRead, stdin};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::mpsc;
|
||||
use std::process::{Command, Child, Stdio, ChildStdin, ChildStdout};
|
||||
|
||||
|
||||
pub struct IpcParent {
|
||||
child: Child,
|
||||
stdin: ChildStdin,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
}
|
||||
|
||||
impl IpcParent {
|
||||
pub fn setup(module: &Module) -> Result<IpcParent> {
|
||||
let exe = match env::current_exe() {
|
||||
Ok(exe) => exe.into_os_string(),
|
||||
_ => OsString::from("sn0int"),
|
||||
};
|
||||
|
||||
let mut child = Command::new(exe)
|
||||
.arg("sandbox")
|
||||
.arg(&module.canonical())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.context("Failed to spawn child process")?;
|
||||
|
||||
let stdin = child.stdin.take().expect("Failed to take child stdin");
|
||||
let stdout = child.stdout.take().expect("Failed to take child stdout");
|
||||
let stdout = BufReader::new(stdout);
|
||||
|
||||
Ok(IpcParent {
|
||||
child,
|
||||
stdin,
|
||||
stdout,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send_start(&mut self, start: &StartCommand) -> Result<()> {
|
||||
let start = serde_json::to_value(&start)?;
|
||||
self.send(&start)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send(&mut self, value: &serde_json::Value) -> Result<()> {
|
||||
let mut value = serde_json::to_string(value)?;
|
||||
value.push('\n');
|
||||
self.stdin.write_all(value.as_bytes())?;
|
||||
debug!("IpcParent sent: {:?}", value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_struct<T: serde::Serialize>(&mut self, value: T, tx: &EventSender) {
|
||||
let value = serde_json::to_value(value).expect("Failed to serialize reply");
|
||||
if let Err(_) = self.send(&value) {
|
||||
tx.send(Event2::Log(LogEvent::Error("Failed to send to child".into())));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recv(&mut self) -> Result<Event> {
|
||||
let mut line = String::new();
|
||||
let len = self.stdout.read_line(&mut line)?;
|
||||
|
||||
let event = serde_json::from_str(&line[..len])?;
|
||||
debug!("IpcParent received: {:?}", event);
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
pub fn wait(&mut self) -> Result<()> {
|
||||
let exit = self.child.wait()
|
||||
.context("Failed to wait for child")?;
|
||||
|
||||
if exit.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Child signaled error")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_event_callback<T: EventWithCallback>(&mut self, event: T, tx: &EventSender)
|
||||
where <T as EventWithCallback>::Payload: serde::Serialize
|
||||
{
|
||||
let (tx2, rx2) = mpsc::channel();
|
||||
tx.send(event.with_callback(tx2));
|
||||
let reply = rx2.recv().unwrap();
|
||||
|
||||
self.send_struct(reply, tx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(module: Module,
|
||||
tx: &EventSender,
|
||||
arg: serde_json::Value,
|
||||
keyring: Vec<KeyRingEntry>,
|
||||
verbose: u64,
|
||||
has_stdin: bool,
|
||||
proxy: Option<SocketAddr>,
|
||||
options: HashMap<String, String>,
|
||||
blobs: Vec<Blob>,
|
||||
) -> Result<ExitEvent> {
|
||||
let dns_config = Resolver::from_system_v4()?;
|
||||
|
||||
let mut reader = if has_stdin {
|
||||
Some(BufReader::new(stdin()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut ipc_parent = IpcParent::setup(&module)?;
|
||||
ipc_parent.send_start(&StartCommand::new(verbose, keyring, dns_config, proxy, options, module, arg, blobs))?;
|
||||
|
||||
let exit = loop {
|
||||
match ipc_parent.recv()? {
|
||||
Event::Log(event) => tx.send(Event2::Log(event)),
|
||||
Event::Database(object) => ipc_parent.send_event_callback(object, &tx),
|
||||
Event::Stdio(object) => object.apply(&mut ipc_parent, tx, &mut reader),
|
||||
Event::Ratelimit(req) => ipc_parent.send_event_callback(req, &tx),
|
||||
Event::Blob(blob) => ipc_parent.send_event_callback(blob, &tx),
|
||||
Event::Exit(event) => {
|
||||
if let ExitEvent::Err(err) = &event {
|
||||
tx.send(Event2::Log(LogEvent::Error(err.clone())));
|
||||
}
|
||||
break event;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
ipc_parent.wait()?;
|
||||
|
||||
Ok(exit)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ impl fmt::Display for KeyName {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct KeyRing {
|
||||
keys: HashMap<String, HashMap<String, Option<String>>>,
|
||||
grants: HashMap<String, HashSet<ModuleID>>,
|
||||
@@ -82,18 +82,19 @@ impl KeyRing {
|
||||
pub fn init() -> Result<KeyRing> {
|
||||
let path = Self::path()?;
|
||||
|
||||
if path.exists() {
|
||||
let keyring = if path.exists() {
|
||||
Self::load(&path)
|
||||
.context("Failed to load keyring")?
|
||||
} else {
|
||||
Ok(KeyRing {
|
||||
keys: HashMap::new(),
|
||||
grants: HashMap::new(),
|
||||
})
|
||||
}
|
||||
KeyRing::default()
|
||||
};
|
||||
|
||||
Ok(keyring)
|
||||
}
|
||||
|
||||
pub fn load(path: &Path) -> Result<KeyRing> {
|
||||
let buf = fs::read(&path)?;
|
||||
let buf = fs::read(&path)
|
||||
.context("Failed to read keyring file")?;
|
||||
serde_json::from_slice(&buf)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
@@ -101,7 +102,8 @@ impl KeyRing {
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let path = Self::path()?;
|
||||
let buf = serde_json::to_string(&self)?;
|
||||
fs::write(&path, buf)?;
|
||||
fs::write(&path, buf)
|
||||
.context("Failed to save keyring")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod args;
|
||||
pub mod auth;
|
||||
pub mod autonoscope;
|
||||
pub mod blobs;
|
||||
pub mod cal;
|
||||
pub mod cmd;
|
||||
pub mod config;
|
||||
use sn0int_std::crt;
|
||||
@@ -27,10 +28,13 @@ pub use sn0int_std::geoip;
|
||||
use sn0int_std::gfx;
|
||||
use sn0int_std::html;
|
||||
use sn0int_std::json;
|
||||
pub mod ipc;
|
||||
pub mod keyring;
|
||||
use sn0int_std::lazy;
|
||||
pub mod migrations;
|
||||
pub mod models;
|
||||
use sn0int_std::mqtt;
|
||||
pub mod notify;
|
||||
pub mod paths;
|
||||
pub use sn0int_std::psl;
|
||||
pub mod options;
|
||||
|
||||
13
src/main.rs
13
src/main.rs
@@ -5,8 +5,9 @@ use sn0int::cmd::{self, LiteCmd};
|
||||
use sn0int::config::Config;
|
||||
use sn0int::db;
|
||||
use sn0int::errors::*;
|
||||
use sn0int::engine::{self, Module};
|
||||
use sn0int::engine::Module;
|
||||
use sn0int::geoip::{GeoIP, AsnDB, Maxmind};
|
||||
use sn0int::ipc;
|
||||
use sn0int::options::Opt;
|
||||
use sn0int::paths;
|
||||
use sn0int::psl::PslReader;
|
||||
@@ -46,7 +47,11 @@ fn run_run(gargs: &Args, args: &args::Run, config: &Config) -> Result<()> {
|
||||
rl.set_target(Some(target));
|
||||
}
|
||||
|
||||
cmd::run_cmd::execute(&mut rl, args.into(), Opt::collect(&args.options))
|
||||
if args.dump_sandbox_init_msg {
|
||||
cmd::run_cmd::dump_sandbox_init_msg(&mut rl, args.into(), Opt::collect(&args.options))
|
||||
} else {
|
||||
cmd::run_cmd::execute(&mut rl, args.into(), Opt::collect(&args.options))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_sandbox() -> Result<()> {
|
||||
@@ -57,7 +62,7 @@ fn run_sandbox() -> Result<()> {
|
||||
|
||||
sandbox::init()
|
||||
.context("Failed to init sandbox")?;
|
||||
engine::isolation::run_worker(geoip, asn, psl)
|
||||
ipc::child::run(geoip, asn, psl)
|
||||
}
|
||||
|
||||
fn run_cmd<T: cmd::Cmd>(gargs: &Args, args: T, config: &Config) -> Result<()> {
|
||||
@@ -120,6 +125,8 @@ fn run() -> Result<()> {
|
||||
Some(SubCommand::Workspace(workspace)) => workspace.run(&config),
|
||||
Some(SubCommand::Fsck(fsck)) => run_cmd(&args, fsck, &config),
|
||||
Some(SubCommand::Export(export)) => run_cmd(&args, export, &config),
|
||||
Some(SubCommand::Cal(cal)) => run_cmd(&args, cal, &config),
|
||||
Some(SubCommand::Notify(notify)) => run_cmd(&args, notify, &config),
|
||||
Some(SubCommand::Repl) => repl::run(&config),
|
||||
Some(SubCommand::Paths) => paths::run(&config),
|
||||
Some(SubCommand::Completions(completions)) => complete::run_generate(&completions),
|
||||
|
||||
@@ -358,13 +358,13 @@ impl Updateable<Account> for AccountUpdate {
|
||||
Self::clear_if_equal(&mut self.profile_pic, &existing.profile_pic);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "displayname", &self.displayname);
|
||||
Self::push_value(updates, "email", &self.email);
|
||||
Self::push_value(updates, "url", &self.url);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen);
|
||||
Self::push_value(updates, "birthday", &self.birthday);
|
||||
Self::push_value(updates, "phonenumber", &self.phonenumber);
|
||||
Self::push_value(updates, "profile_pic", &self.profile_pic);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "displayname", &self.displayname, colors);
|
||||
Self::push_value(updates, "email", &self.email, colors);
|
||||
Self::push_value(updates, "url", &self.url, colors);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen, colors);
|
||||
Self::push_value(updates, "birthday", &self.birthday, colors);
|
||||
Self::push_value(updates, "phonenumber", &self.phonenumber, colors);
|
||||
Self::push_value(updates, "profile_pic", &self.profile_pic, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,16 +37,19 @@ impl Activity {
|
||||
let mut query = activity.into_boxed();
|
||||
|
||||
if let Some(my_topic) = &filter.topic {
|
||||
debug!("Filtering topic to be {:?}", my_topic);
|
||||
query = query.filter(topic.eq(my_topic));
|
||||
}
|
||||
|
||||
// "since" filter is not applied
|
||||
|
||||
if let Some(until) = &filter.until {
|
||||
debug!("Filtering until <= {}", until);
|
||||
query = query.filter(time.le(until));
|
||||
}
|
||||
|
||||
if filter.location {
|
||||
debug!("Filtering latitude and longitude != null");
|
||||
query = query
|
||||
.filter(latitude.is_not_null())
|
||||
.filter(longitude.is_not_null());
|
||||
@@ -61,6 +64,7 @@ impl Activity {
|
||||
let mut query = Self::build_query_except_since(filter);
|
||||
|
||||
if let Some(since) = &filter.since {
|
||||
debug!("Filtering since >= {}", since);
|
||||
query = query.filter(time.ge(since));
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ impl Updateable<BreachEmail> for BreachEmailUpdate {
|
||||
Self::clear_if_equal(&mut self.password, &existing.password);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "password", &self.password);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "password", &self.password, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,14 +342,14 @@ impl Updateable<CryptoAddr> for CryptoAddrUpdate {
|
||||
Self::clear_if_equal(&mut self.description, &existing.description);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "currency", &self.currency);
|
||||
Self::push_value(updates, "denominator", &self.denominator);
|
||||
Self::push_value(updates, "balance", &self.balance);
|
||||
Self::push_value(updates, "received", &self.received);
|
||||
Self::push_value(updates, "first_seen", &self.first_seen);
|
||||
Self::push_value(updates, "last_withdrawal", &self.last_withdrawal);
|
||||
Self::push_value(updates, "description", &self.description);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "currency", &self.currency, colors);
|
||||
Self::push_value(updates, "denominator", &self.denominator, colors);
|
||||
Self::push_value(updates, "balance", &self.balance, colors);
|
||||
Self::push_value(updates, "received", &self.received, colors);
|
||||
Self::push_value(updates, "first_seen", &self.first_seen, colors);
|
||||
Self::push_value(updates, "last_withdrawal", &self.last_withdrawal, colors);
|
||||
Self::push_value(updates, "description", &self.description, colors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -332,10 +332,10 @@ impl Updateable<Device> for DeviceUpdate {
|
||||
Self::clear_if_equal(&mut self.last_seen, &existing.last_seen);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "name", &self.name);
|
||||
Self::push_value(updates, "hostname", &self.hostname);
|
||||
Self::push_value(updates, "vendor", &self.vendor);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "name", &self.name, colors);
|
||||
Self::push_value(updates, "hostname", &self.hostname, colors);
|
||||
Self::push_value(updates, "vendor", &self.vendor, colors);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,24 +122,6 @@ impl Domain {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrintableDomain {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for PrintableDomain {
|
||||
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(w, "{:?}", self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Printable<PrintableDomain> for Domain {
|
||||
fn printable(&self, _db: &Database) -> Result<PrintableDomain> {
|
||||
Ok(PrintableDomain {
|
||||
value: self.value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DetailedDomain {
|
||||
id: i32,
|
||||
value: String,
|
||||
@@ -222,14 +204,6 @@ impl Upsertable<Domain> for NewDomain {
|
||||
}
|
||||
}
|
||||
|
||||
impl Printable<PrintableDomain> for NewDomain {
|
||||
fn printable(&self, _db: &Database) -> Result<PrintableDomain> {
|
||||
Ok(PrintableDomain {
|
||||
value: self.value.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InsertDomain {
|
||||
pub value: String,
|
||||
@@ -240,6 +214,9 @@ impl InsertToNew for InsertDomain {
|
||||
|
||||
fn try_into_new(self) -> Result<NewDomain> {
|
||||
let value = self.value.to_lowercase();
|
||||
if value.contains('*') {
|
||||
bail!("Asterisks inside domains are not valid");
|
||||
}
|
||||
Ok(NewDomain {
|
||||
value,
|
||||
unscoped: false,
|
||||
|
||||
@@ -335,9 +335,9 @@ impl Updateable<Email> for EmailUpdate {
|
||||
Self::clear_if_equal(&mut self.valid, &existing.valid);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "displayname", &self.displayname);
|
||||
Self::push_value(updates, "valid", &self.valid);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "displayname", &self.displayname, colors);
|
||||
Self::push_value(updates, "valid", &self.valid, colors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -464,19 +464,19 @@ impl Updateable<Image> for ImageUpdate {
|
||||
Self::clear_if_equal(&mut self.phash, &existing.phash);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "filename", &self.filename);
|
||||
Self::push_value(updates, "mime", &self.mime);
|
||||
Self::push_value(updates, "width", &self.width);
|
||||
Self::push_value(updates, "height", &self.height);
|
||||
Self::push_value(updates, "created", &self.created);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "filename", &self.filename, colors);
|
||||
Self::push_value(updates, "mime", &self.mime, colors);
|
||||
Self::push_value(updates, "width", &self.width, colors);
|
||||
Self::push_value(updates, "height", &self.height, colors);
|
||||
Self::push_value(updates, "created", &self.created, colors);
|
||||
|
||||
Self::push_value(updates, "latitude", &self.latitude);
|
||||
Self::push_value(updates, "longitude", &self.longitude);
|
||||
Self::push_value(updates, "latitude", &self.latitude, colors);
|
||||
Self::push_value(updates, "longitude", &self.longitude, colors);
|
||||
|
||||
Self::push_value(updates, "nudity", &self.nudity);
|
||||
Self::push_value(updates, "ahash", &self.ahash);
|
||||
Self::push_value(updates, "dhash", &self.dhash);
|
||||
Self::push_value(updates, "phash", &self.phash);
|
||||
Self::push_value(updates, "nudity", &self.nudity, colors);
|
||||
Self::push_value(updates, "ahash", &self.ahash, colors);
|
||||
Self::push_value(updates, "dhash", &self.dhash, colors);
|
||||
Self::push_value(updates, "phash", &self.phash, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,17 +444,17 @@ impl Updateable<IpAddr> for IpAddrUpdate {
|
||||
Self::clear_if_equal(&mut self.reverse_dns, &existing.reverse_dns);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "continent", &self.continent);
|
||||
Self::push_value(updates, "continent_code", &self.continent_code);
|
||||
Self::push_value(updates, "country", &self.country);
|
||||
Self::push_value(updates, "country_code", &self.country_code);
|
||||
Self::push_value(updates, "city", &self.city);
|
||||
Self::push_value(updates, "latitude", &self.latitude);
|
||||
Self::push_value(updates, "longitude", &self.longitude);
|
||||
Self::push_value(updates, "asn", &self.asn);
|
||||
Self::push_value(updates, "as_org", &self.as_org);
|
||||
Self::push_value(updates, "description", &self.description);
|
||||
Self::push_value(updates, "reverse_dns", &self.reverse_dns);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "continent", &self.continent, colors);
|
||||
Self::push_value(updates, "continent_code", &self.continent_code, colors);
|
||||
Self::push_value(updates, "country", &self.country, colors);
|
||||
Self::push_value(updates, "country_code", &self.country_code, colors);
|
||||
Self::push_value(updates, "city", &self.city, colors);
|
||||
Self::push_value(updates, "latitude", &self.latitude, colors);
|
||||
Self::push_value(updates, "longitude", &self.longitude, colors);
|
||||
Self::push_value(updates, "asn", &self.asn, colors);
|
||||
Self::push_value(updates, "as_org", &self.as_org, colors);
|
||||
Self::push_value(updates, "description", &self.description, colors);
|
||||
Self::push_value(updates, "reverse_dns", &self.reverse_dns, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::db::{Database, Table, Filter, Family};
|
||||
use crate::engine::ctx::State;
|
||||
use crate::errors::*;
|
||||
use crate::db::{Database, Table, Filter};
|
||||
use crate::fmt;
|
||||
use crate::schema::*;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
use crate::engine::ctx::State;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -28,39 +29,39 @@ pub enum Insert {
|
||||
}
|
||||
|
||||
impl Insert {
|
||||
pub fn label(&self, db: &Database) -> Result<String> {
|
||||
let label = match self {
|
||||
Insert::Domain(x) => format!("{:?}", x.value),
|
||||
Insert::Subdomain(x) => format!("{:?}", x.value),
|
||||
Insert::IpAddr(x) => format!("{:?}", x.value),
|
||||
pub fn value(&self, db: &Database) -> Result<Cow<String>> {
|
||||
let value = match self {
|
||||
Insert::Domain(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Subdomain(x) => Cow::Borrowed(&x.value),
|
||||
Insert::IpAddr(x) => Cow::Borrowed(&x.value),
|
||||
Insert::SubdomainIpAddr(x) => {
|
||||
let subdomain = Subdomain::by_id(db, x.subdomain_id)?;
|
||||
let ipaddr = IpAddr::by_id(db, x.ip_addr_id)?;
|
||||
format!("{:?}+{:?}", subdomain.value, ipaddr.value)
|
||||
Cow::Owned(format!("{}+{}", subdomain.value, ipaddr.value))
|
||||
},
|
||||
Insert::Url(x) => format!("{:?}", x.value),
|
||||
Insert::Email(x) => format!("{:?}", x.value),
|
||||
Insert::PhoneNumber(x) => format!("{:?}", x.value),
|
||||
Insert::Device(x) => format!("{:?}", x.value),
|
||||
Insert::Network(x) => format!("{:?}", x.value),
|
||||
Insert::Url(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Email(x) => Cow::Borrowed(&x.value),
|
||||
Insert::PhoneNumber(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Device(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Network(x) => Cow::Borrowed(&x.value),
|
||||
Insert::NetworkDevice(x) => {
|
||||
let network = Network::by_id(db, x.network_id)?;
|
||||
let device = Device::by_id(db, x.device_id)?;
|
||||
format!("{:?}+{:?}", network.value, device.value)
|
||||
Cow::Owned(format!("{}+{}", network.value, device.value))
|
||||
},
|
||||
Insert::Account(x) => format!("{:?}", x.value),
|
||||
Insert::Breach(x) => format!("{:?}", x.value),
|
||||
Insert::Account(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Breach(x) => Cow::Borrowed(&x.value),
|
||||
Insert::BreachEmail(x) => {
|
||||
let breach = Breach::by_id(db, x.breach_id)?;
|
||||
let email = Email::by_id(db, x.email_id)?;
|
||||
format!("{:?}+{:?}", breach.value, email.value)
|
||||
Cow::Owned(format!("{}+{}", breach.value, email.value))
|
||||
}
|
||||
Insert::Image(x) => format!("{:?}", x.value),
|
||||
Insert::Port(x) => format!("{:?}", x.value),
|
||||
Insert::Netblock(x) => format!("{:?}", x.value),
|
||||
Insert::CryptoAddr(x) => format!("{:?}", x.value),
|
||||
Insert::Image(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Port(x) => Cow::Borrowed(&x.value),
|
||||
Insert::Netblock(x) => Cow::Borrowed(&x.value),
|
||||
Insert::CryptoAddr(x) => Cow::Borrowed(&x.value),
|
||||
};
|
||||
Ok(label)
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -68,26 +69,27 @@ impl Insert {
|
||||
Table::from(self).into()
|
||||
}
|
||||
|
||||
pub fn printable(&self, db: &Database) -> Result<String> {
|
||||
Ok(match self {
|
||||
Insert::Domain(x) => format!("Domain: {}", x.printable(db)?),
|
||||
Insert::Subdomain(x) => format!("Subdomain: {}", x.printable(db)?),
|
||||
Insert::IpAddr(x) => format!("IpAddr: {}", x.printable(db)?),
|
||||
Insert::SubdomainIpAddr(x) => x.printable(db)?.to_string(),
|
||||
Insert::Url(x) => format!("Url: {}", x.printable(db)?),
|
||||
Insert::Email(x) => format!("Email: {}", x.printable(db)?),
|
||||
Insert::PhoneNumber(x) => format!("PhoneNumber: {}", x.printable(db)?),
|
||||
Insert::Device(x) => format!("Device: {}", x.printable(db)?),
|
||||
Insert::Network(x) => format!("Network: {}", x.printable(db)?),
|
||||
Insert::NetworkDevice(x) => x.printable(db)?.to_string(),
|
||||
Insert::Account(x) => format!("Account: {}", x.printable(db)?),
|
||||
Insert::Breach(x) => format!("Breach: {}", x.printable(db)?),
|
||||
Insert::BreachEmail(x) => x.printable(db)?.to_string(),
|
||||
Insert::Image(x) => format!("Image: {}", x.printable(db)?),
|
||||
Insert::Port(x) => format!("Port: {}", x.printable(db)?),
|
||||
Insert::Netblock(x) => format!("Netblock: {}", x.printable(db)?),
|
||||
Insert::CryptoAddr(x) => format!("CryptoAddr: {}", x.printable(db)?),
|
||||
})
|
||||
#[inline]
|
||||
pub fn family(&self) -> &str {
|
||||
match self {
|
||||
Insert::Domain(_) => Family::Domain.as_str(),
|
||||
Insert::Subdomain(_) => Family::Subdomain.as_str(),
|
||||
Insert::IpAddr(_) => Family::Ipaddr.as_str(),
|
||||
Insert::SubdomainIpAddr(_) => Family::SubdomainIpaddr.as_str(),
|
||||
Insert::Url(_) => Family::Url.as_str(),
|
||||
Insert::Email(_) => Family::Email.as_str(),
|
||||
Insert::PhoneNumber(_) => Family::Phonenumber.as_str(),
|
||||
Insert::Device(_) => Family::Device.as_str(),
|
||||
Insert::Network(_) => Family::Network.as_str(),
|
||||
Insert::NetworkDevice(_) => Family::NetworkDevice.as_str(),
|
||||
Insert::Account(_) => Family::Account.as_str(),
|
||||
Insert::Breach(_) => Family::Breach.as_str(),
|
||||
Insert::BreachEmail(_) => Family::BreachEmail.as_str(),
|
||||
Insert::Image(_) => Family::Image.as_str(),
|
||||
Insert::Port(_) => Family::Port.as_str(),
|
||||
Insert::Netblock(_) => Family::Netblock.as_str(),
|
||||
Insert::CryptoAddr(_) => Family::Cryptoaddr.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ impl From<&Insert> for Table {
|
||||
pub enum Update {
|
||||
Subdomain(SubdomainUpdate),
|
||||
IpAddr(IpAddrUpdate),
|
||||
Url(UrlUpdate),
|
||||
Url(UrlChangeset),
|
||||
Email(EmailUpdate),
|
||||
PhoneNumber(PhoneNumberUpdate),
|
||||
Device(DeviceUpdate),
|
||||
@@ -152,25 +154,42 @@ impl Update {
|
||||
Update::CryptoAddr(update) => update.is_dirty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Update {
|
||||
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
|
||||
pub fn to_plain_str(&self) -> String {
|
||||
match self {
|
||||
Update::Subdomain(update) => write!(w, "{}", update.to_string()),
|
||||
Update::IpAddr(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Url(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Email(update) => write!(w, "{}", update.to_string()),
|
||||
Update::PhoneNumber(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Device(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Network(update) => write!(w, "{}", update.to_string()),
|
||||
Update::NetworkDevice(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Account(update) => write!(w, "{}", update.to_string()),
|
||||
Update::BreachEmail(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Image(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Port(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Netblock(update) => write!(w, "{}", update.to_string()),
|
||||
Update::CryptoAddr(update) => write!(w, "{}", update.to_string()),
|
||||
Update::Subdomain(update) => update.to_plain_str(),
|
||||
Update::IpAddr(update) => update.to_plain_str(),
|
||||
Update::Url(update) => update.to_plain_str(),
|
||||
Update::Email(update) => update.to_plain_str(),
|
||||
Update::PhoneNumber(update) => update.to_plain_str(),
|
||||
Update::Device(update) => update.to_plain_str(),
|
||||
Update::Network(update) => update.to_plain_str(),
|
||||
Update::NetworkDevice(update) => update.to_plain_str(),
|
||||
Update::Account(update) => update.to_plain_str(),
|
||||
Update::BreachEmail(update) => update.to_plain_str(),
|
||||
Update::Image(update) => update.to_plain_str(),
|
||||
Update::Port(update) => update.to_plain_str(),
|
||||
Update::Netblock(update) => update.to_plain_str(),
|
||||
Update::CryptoAddr(update) => update.to_plain_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_term_str(&self) -> String {
|
||||
match self {
|
||||
Update::Subdomain(update) => update.to_term_str(),
|
||||
Update::IpAddr(update) => update.to_term_str(),
|
||||
Update::Url(update) => update.to_term_str(),
|
||||
Update::Email(update) => update.to_term_str(),
|
||||
Update::PhoneNumber(update) => update.to_term_str(),
|
||||
Update::Device(update) => update.to_term_str(),
|
||||
Update::Network(update) => update.to_term_str(),
|
||||
Update::NetworkDevice(update) => update.to_term_str(),
|
||||
Update::Account(update) => update.to_term_str(),
|
||||
Update::BreachEmail(update) => update.to_term_str(),
|
||||
Update::Image(update) => update.to_term_str(),
|
||||
Update::Port(update) => update.to_term_str(),
|
||||
Update::Netblock(update) => update.to_term_str(),
|
||||
Update::CryptoAddr(update) => update.to_term_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,9 +293,15 @@ impl Upsert for NullUpdate {
|
||||
}
|
||||
|
||||
pub trait Updateable<M> {
|
||||
fn to_string(&self) -> String {
|
||||
fn to_plain_str(&self) -> String {
|
||||
let mut updates = Vec::new();
|
||||
self.fmt(&mut updates);
|
||||
self.fmt(&mut updates, false);
|
||||
updates.join(", ")
|
||||
}
|
||||
|
||||
fn to_term_str(&self) -> String {
|
||||
let mut updates = Vec::new();
|
||||
self.fmt(&mut updates, true);
|
||||
updates.join(", ")
|
||||
}
|
||||
|
||||
@@ -304,22 +329,31 @@ pub trait Updateable<M> {
|
||||
fn changeset(&mut self, existing: &M);
|
||||
|
||||
#[inline]
|
||||
fn push_value<D: fmt::Debug>(updates: &mut Vec<String>, name: &str, value: &Option<D>) {
|
||||
fn push_value<D: fmt::Debug>(updates: &mut Vec<String>, name: &str, value: &Option<D>, colors: bool) {
|
||||
if let Some(v) = value {
|
||||
updates.push(format!("{} => \x1b[33m{:?}\x1b[0m", name, v));
|
||||
if colors {
|
||||
updates.push(format!("{} => \x1b[33m{:?}\x1b[0m", name, v));
|
||||
} else {
|
||||
updates.push(format!("{} => {:?}", name, v));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_raw<T: AsRef<str>>(updates: &mut Vec<String>, name: &str, value: Option<T>) {
|
||||
fn push_raw<T: AsRef<str>>(updates: &mut Vec<String>, name: &str, value: Option<T>, colors: bool) {
|
||||
if let Some(v) = value {
|
||||
updates.push(format!("{} => \x1b[33m{}\x1b[0m", name, v.as_ref()));
|
||||
if colors {
|
||||
updates.push(format!("{} => \x1b[33m{}\x1b[0m", name, v.as_ref()));
|
||||
} else {
|
||||
updates.push(format!("{} => {}", name, v.as_ref()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool);
|
||||
}
|
||||
|
||||
// TODO: Printable could probably be dropped
|
||||
pub trait Printable<T: Sized> {
|
||||
fn printable(&self, db: &Database) -> Result<T>;
|
||||
}
|
||||
@@ -374,6 +408,10 @@ impl<T: InsertToNew> LuaInsertToNew for T {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UpdateToChangeset<T> {
|
||||
fn try_into_changeset(self) -> Result<T>;
|
||||
}
|
||||
|
||||
mod domain;
|
||||
pub use self::domain::*;
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ impl DisplayableDetailed for DetailedNetblock {
|
||||
#[inline]
|
||||
fn print(&self, w: &mut fmt::DetailFormatter) -> fmt::Result {
|
||||
w.id(self.id)?;
|
||||
w.debug::<Green, _>(&self.value)?;
|
||||
w.display::<Green, _>(&self.value)?;
|
||||
|
||||
w.start_group();
|
||||
w.opt_debug::<Yellow, _>(&self.asn)?;
|
||||
@@ -307,9 +307,9 @@ impl Updateable<Netblock> for NetblockUpdate {
|
||||
Self::clear_if_equal(&mut self.description, &existing.description);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "asn", &self.asn);
|
||||
Self::push_value(updates, "as_org", &self.as_org);
|
||||
Self::push_value(updates, "description", &self.description);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "asn", &self.asn, colors);
|
||||
Self::push_value(updates, "as_org", &self.as_org, colors);
|
||||
Self::push_value(updates, "description", &self.description, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +310,9 @@ impl Updateable<Network> for NetworkUpdate {
|
||||
Self::clear_if_equal(&mut self.description, &existing.description);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "latitude", &self.latitude);
|
||||
Self::push_value(updates, "longitude", &self.longitude);
|
||||
Self::push_value(updates, "description", &self.description);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "latitude", &self.latitude, colors);
|
||||
Self::push_value(updates, "longitude", &self.longitude, colors);
|
||||
Self::push_value(updates, "description", &self.description, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +194,8 @@ impl Updateable<NetworkDevice> for NetworkDeviceUpdate {
|
||||
Self::clear_if_equal(&mut self.last_seen, &existing.last_seen);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "ipaddr", &self.ipaddr);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "ipaddr", &self.ipaddr, colors);
|
||||
Self::push_value(updates, "last_seen", &self.last_seen, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -377,16 +377,16 @@ impl Updateable<PhoneNumber> for PhoneNumberUpdate {
|
||||
Self::clear_if_equal(&mut self.caller_type, &existing.caller_type);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "name", &self.name);
|
||||
Self::push_value(updates, "valid", &self.valid);
|
||||
Self::push_value(updates, "last_online", &self.last_online);
|
||||
Self::push_value(updates, "country", &self.country);
|
||||
Self::push_value(updates, "carrier", &self.carrier);
|
||||
Self::push_value(updates, "line", &self.line);
|
||||
Self::push_value(updates, "is_ported", &self.is_ported);
|
||||
Self::push_value(updates, "last_ported", &self.last_ported);
|
||||
Self::push_value(updates, "caller_name", &self.caller_name);
|
||||
Self::push_value(updates, "caller_type", &self.caller_type);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "name", &self.name, colors);
|
||||
Self::push_value(updates, "valid", &self.valid, colors);
|
||||
Self::push_value(updates, "last_online", &self.last_online, colors);
|
||||
Self::push_value(updates, "country", &self.country, colors);
|
||||
Self::push_value(updates, "carrier", &self.carrier, colors);
|
||||
Self::push_value(updates, "line", &self.line, colors);
|
||||
Self::push_value(updates, "is_ported", &self.is_ported, colors);
|
||||
Self::push_value(updates, "last_ported", &self.last_ported, colors);
|
||||
Self::push_value(updates, "caller_name", &self.caller_name, colors);
|
||||
Self::push_value(updates, "caller_type", &self.caller_type, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,10 +337,10 @@ impl Updateable<Port> for PortUpdate {
|
||||
Self::clear_if_equal(&mut self.version, &existing.version);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "status", &self.status);
|
||||
Self::push_value(updates, "banner", &self.banner);
|
||||
Self::push_value(updates, "service", &self.service);
|
||||
Self::push_value(updates, "version", &self.version);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "status", &self.status, colors);
|
||||
Self::push_value(updates, "banner", &self.banner, colors);
|
||||
Self::push_value(updates, "service", &self.service, colors);
|
||||
Self::push_value(updates, "version", &self.version, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +258,9 @@ impl InsertToNew for InsertSubdomain {
|
||||
|
||||
fn try_into_new(self) -> Result<NewSubdomain> {
|
||||
let value = self.value.to_lowercase();
|
||||
if value.contains('*') {
|
||||
bail!("Asterisks inside domains are not valid");
|
||||
}
|
||||
Ok(NewSubdomain {
|
||||
domain_id: self.domain_id,
|
||||
value,
|
||||
@@ -294,7 +297,7 @@ impl Updateable<Subdomain> for SubdomainUpdate {
|
||||
Self::clear_if_equal(&mut self.resolvable, &existing.resolvable);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "resolvable", &self.resolvable);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "resolvable", &self.resolvable, colors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::ser::StringOrBytes;
|
||||
use crate::errors::*;
|
||||
use crate::fmt::Write;
|
||||
use crate::fmt::colors::*;
|
||||
use crate::models::*;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use crate::ser;
|
||||
use crate::url;
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ pub struct NewUrl {
|
||||
pub value: String,
|
||||
pub path: String,
|
||||
pub status: Option<i32>,
|
||||
#[serde(deserialize_with="ser::opt_string_or_bytes")]
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub online: Option<bool>,
|
||||
pub title: Option<String>,
|
||||
@@ -254,7 +253,7 @@ impl InsertableStruct<Url> for NewUrl {
|
||||
}
|
||||
|
||||
impl Upsertable<Url> for NewUrl {
|
||||
type Update = UrlUpdate;
|
||||
type Update = UrlChangeset;
|
||||
|
||||
fn upsert(self, existing: &Url) -> Self::Update {
|
||||
Self::Update {
|
||||
@@ -283,8 +282,7 @@ pub struct InsertUrl {
|
||||
pub subdomain_id: i32,
|
||||
pub value: String,
|
||||
pub status: Option<i32>,
|
||||
#[serde(deserialize_with="ser::opt_string_or_bytes")]
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub body: Option<StringOrBytes>,
|
||||
pub online: Option<bool>,
|
||||
pub title: Option<String>,
|
||||
pub redirect: Option<String>,
|
||||
@@ -309,7 +307,7 @@ impl InsertToNew for InsertUrl {
|
||||
value: self.value,
|
||||
path,
|
||||
status: self.status,
|
||||
body: self.body,
|
||||
body: self.body.map(|x| x.0),
|
||||
online: self.online,
|
||||
title: self.title,
|
||||
redirect,
|
||||
@@ -320,7 +318,7 @@ impl InsertToNew for InsertUrl {
|
||||
|
||||
#[derive(Identifiable, AsChangeset, Serialize, Deserialize, Debug)]
|
||||
#[table_name="urls"]
|
||||
pub struct UrlUpdate {
|
||||
pub struct UrlChangeset {
|
||||
pub id: i32,
|
||||
pub status: Option<i32>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
@@ -329,7 +327,7 @@ pub struct UrlUpdate {
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
impl Upsert for UrlUpdate {
|
||||
impl Upsert for UrlChangeset {
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.status.is_some() ||
|
||||
self.body.is_some() ||
|
||||
@@ -347,7 +345,7 @@ impl Upsert for UrlUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
impl Updateable<Url> for UrlUpdate {
|
||||
impl Updateable<Url> for UrlChangeset {
|
||||
fn changeset(&mut self, existing: &Url) {
|
||||
Self::clear_if_equal(&mut self.online, &existing.online);
|
||||
Self::clear_if_equal(&mut self.status, &existing.status);
|
||||
@@ -356,12 +354,36 @@ impl Updateable<Url> for UrlUpdate {
|
||||
Self::clear_if_equal(&mut self.redirect, &existing.redirect);
|
||||
}
|
||||
|
||||
fn fmt(&self, updates: &mut Vec<String>) {
|
||||
Self::push_value(updates, "online", &self.online);
|
||||
Self::push_value(updates, "status", &self.status);
|
||||
Self::push_raw(updates, "body", self.body.as_ref().map(|x| format!("[{} bytes]", x.len())));
|
||||
Self::push_value(updates, "title", &self.title);
|
||||
Self::push_value(updates, "redirect", &self.redirect);
|
||||
fn fmt(&self, updates: &mut Vec<String>, colors: bool) {
|
||||
Self::push_value(updates, "online", &self.online, colors);
|
||||
Self::push_value(updates, "status", &self.status, colors);
|
||||
Self::push_raw(updates, "body", self.body.as_ref().map(|x| format!("[{} bytes]", x.len())), colors);
|
||||
Self::push_value(updates, "title", &self.title, colors);
|
||||
Self::push_value(updates, "redirect", &self.redirect, colors);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UrlUpdate {
|
||||
pub id: i32,
|
||||
pub status: Option<i32>,
|
||||
pub body: Option<StringOrBytes>,
|
||||
pub online: Option<bool>,
|
||||
pub title: Option<String>,
|
||||
pub redirect: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateToChangeset<UrlChangeset> for UrlUpdate {
|
||||
fn try_into_changeset(self) -> Result<UrlChangeset> {
|
||||
// TODO: redirect needs pre-processing
|
||||
Ok(UrlChangeset {
|
||||
id: self.id,
|
||||
status: self.status,
|
||||
body: self.body.map(|x| x.0),
|
||||
online: self.online,
|
||||
title: self.title,
|
||||
redirect: self.redirect,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
247
src/notify/mod.rs
Normal file
247
src/notify/mod.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use crate::errors::*;
|
||||
use crate::cmd::run_cmd::prepare_keyring;
|
||||
use crate::cmd::run_cmd::Params;
|
||||
use crate::engine::Module;
|
||||
use crate::options;
|
||||
use crate::shell::Shell;
|
||||
use crate::term::SpinLogger;
|
||||
use crate::worker;
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use serde::ser::{Serialize, Serializer};
|
||||
use sn0int_common::metadata::Source;
|
||||
use sn0int_std::blobs::Blob;
|
||||
use sn0int_std::ratelimits::Ratelimiter;
|
||||
use std::collections::HashMap;
|
||||
use std::result;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, StructOpt, Serialize)]
|
||||
pub struct Notification {
|
||||
pub subject: String,
|
||||
pub body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct NotificationConfig {
|
||||
#[serde(default)]
|
||||
pub workspaces: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub topics: Vec<Glob>,
|
||||
pub script: String,
|
||||
#[serde(default)]
|
||||
pub options: Vec<options::Opt>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Glob {
|
||||
patterns: Vec<glob::Pattern>,
|
||||
src: String,
|
||||
}
|
||||
|
||||
impl Glob {
|
||||
fn matches(&self, topic: &str) -> bool {
|
||||
let mut filter = self.patterns.iter();
|
||||
let mut topic = topic.split(':');
|
||||
|
||||
loop {
|
||||
match (filter.next(), topic.next()) {
|
||||
(Some(filter), Some(topic)) => if !filter.matches(&topic) {
|
||||
return false;
|
||||
},
|
||||
(None, None) => return true,
|
||||
(_, _) => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Glob {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Glob> {
|
||||
let patterns = s.split(':')
|
||||
.map(|s| glob::Pattern::new(s).map_err(Error::from))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(Glob {
|
||||
patterns,
|
||||
src: s.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Glob {
|
||||
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
serializer.serialize_str(&self.src)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Glob {
|
||||
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_rule<T>(name: &str, filter: &[T], value: &str, cmp: fn(&T, &str) -> bool) -> bool {
|
||||
if !filter.is_empty() {
|
||||
debug!("{} filter is active", name);
|
||||
if !filter.iter().any(|filter| cmp(filter, value)) {
|
||||
debug!("{} isn't allow-listed, aborting", name);
|
||||
return false;
|
||||
}
|
||||
debug!("{} was allow-listed", name);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
impl NotificationConfig {
|
||||
fn matches(&self, name: &str, workspace: &str, topic: &str) -> bool {
|
||||
debug!("Testing notification with rules: {:?}", name);
|
||||
if !apply_rule("workspace", &self.workspaces, workspace, |filter, value| filter == value) {
|
||||
return false;
|
||||
}
|
||||
if !apply_rule("topic", &self.topics, topic, |filter, value| filter.matches(value)) {
|
||||
return false;
|
||||
}
|
||||
debug!("Notification matches this config");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trigger_notify_event<T: SpinLogger>(rl: &mut Shell, spinner: &mut T, ratelimit: &mut Ratelimiter, topic: &str, notification: &Notification) -> Result<()> {
|
||||
run_router(rl, spinner, ratelimit, false, topic, notification)
|
||||
}
|
||||
|
||||
fn prepare_arg(notification: &Notification) -> Result<(serde_json::Value, Option<String>, Vec<Blob>)> {
|
||||
let arg = serde_json::to_value(notification)?;
|
||||
Ok((arg, None, vec![]))
|
||||
}
|
||||
|
||||
pub fn exec(rl: &mut Shell, module: &Module, ratelimit: &mut Ratelimiter, options: HashMap<String, String>, verbose: u64, notification: &Notification) -> Result<usize> {
|
||||
let module_name = module.canonical();
|
||||
debug!("Setting up notification execution with {:?}", module_name);
|
||||
|
||||
if *module.source() != Some(Source::Notifications) {
|
||||
bail!("Module doesn't take notifications as source");
|
||||
}
|
||||
|
||||
let params = Params {
|
||||
threads: 1,
|
||||
verbose,
|
||||
stdin: false,
|
||||
grants: &[],
|
||||
grant_full_keyring: false,
|
||||
deny_keyring: false,
|
||||
exit_on_error: false,
|
||||
};
|
||||
|
||||
prepare_keyring(rl.keyring_mut(), &module, ¶ms)?;
|
||||
let args = vec![prepare_arg(¬ification)?];
|
||||
|
||||
debug!("Executing notification module {:?}", module_name);
|
||||
let errors = worker::spawn(rl, &module, ratelimit, args, ¶ms, rl.config().network.proxy.clone(), options);
|
||||
debug!("Notification module {:?} exited with {:?} errors", module_name, errors);
|
||||
|
||||
Ok(errors)
|
||||
}
|
||||
|
||||
pub fn run_router<T: SpinLogger>(rl: &mut Shell, spinner: &mut T, ratelimit: &mut Ratelimiter, dry_run: bool, topic: &str, notification: &Notification) -> Result<()> {
|
||||
let configs = rl.config().notifications.clone();
|
||||
|
||||
debug!("Running notification router");
|
||||
for (name, config) in configs {
|
||||
if rl.signal_register().ctrlc_received() {
|
||||
debug!("Exiting notification router due to ctrl-c");
|
||||
break;
|
||||
}
|
||||
|
||||
if config.matches(&name, rl.workspace(), topic) {
|
||||
let module = rl.library().get(&config.script)?.clone();
|
||||
if dry_run {
|
||||
spinner.success(&format!("Executed {} for {:?} (dry-run)", module.canonical(), name));
|
||||
} else {
|
||||
let options = options::Opt::collect(&config.options);
|
||||
match exec(rl, &module, ratelimit, options, 0, notification) {
|
||||
Ok(0) => {
|
||||
let msg = format!("Executed {} for {:?}", module.canonical(), name);
|
||||
spinner.success(&msg);
|
||||
},
|
||||
Ok(errors) => {
|
||||
let msg = format!("Executed {} for {:?} ({} errors)", module.canonical(), name, errors);
|
||||
spinner.error(&msg);
|
||||
},
|
||||
Err(err) => {
|
||||
spinner.error(&format!("Fatal {} for {:?}: {}", module.canonical(), name, err));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Notification router finished");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn match_topic_str(filter: &str, value: &str) -> bool {
|
||||
let filter: Glob = filter.parse().unwrap();
|
||||
filter.matches(value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_exact() {
|
||||
assert!(match_topic_str("topic:hello-world", "topic:hello-world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_starts_with() {
|
||||
assert!(match_topic_str("topic:*", "topic:hello-world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_ends_with() {
|
||||
assert!(match_topic_str("*:hello-world", "topic:hello-world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_one_wildcard_one_section() {
|
||||
assert!(match_topic_str("a:*:z", "a:b:z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_one_wildcard_not_two_sections() {
|
||||
assert!(!match_topic_str("a:*:z", "a:b:c:z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_two_wildcards_two_sections() {
|
||||
assert!(match_topic_str("a:*:*:z", "a:b:c:z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_one_wildcard_not_two_sections_start() {
|
||||
assert!(!match_topic_str("a:*", "a:b:c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_one_wildcard_not_two_sections_end() {
|
||||
assert!(!match_topic_str("*:z", "b:c:z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_many_wildcards() {
|
||||
assert!(match_topic_str("a:*:*:d:e:*:g:*:z", "a:b:c:d:e:f:g:h:z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_topic_empty_filter() {
|
||||
assert!(!match_topic_str("", "abc"));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::errors::*;
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use serde::ser::{Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::result;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Opt {
|
||||
key: String,
|
||||
value: String,
|
||||
@@ -32,3 +34,26 @@ impl FromStr for Opt {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Opt {
|
||||
fn to_string(&self) -> String {
|
||||
format!("{}={}", self.key, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Opt {
|
||||
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Opt {
|
||||
fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
|
||||
where D: Deserializer<'de>
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
FromStr::from_str(&s).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::engine::{ctx, Environment, DummyReporter};
|
||||
use crate::engine::{ctx, Environment};
|
||||
use crate::ipc::child::DummyIpcChild;
|
||||
use crate::engine::ctx::{State, LuaState};
|
||||
use crate::geoip::{Maxmind, AsnDB, GeoIP};
|
||||
use crate::hlua::{Lua, AnyLuaValue};
|
||||
@@ -81,7 +82,7 @@ impl<'a> Repl<'a> {
|
||||
|
||||
pub fn run(config: &Config) -> Result<()> {
|
||||
let keyring = Vec::new();
|
||||
let dns_config = Resolver::from_system()?;
|
||||
let dns_config = Resolver::from_system_v4()?;
|
||||
let proxy = config.network.proxy;
|
||||
|
||||
let cache_dir = paths::cache_dir()?;
|
||||
@@ -90,7 +91,7 @@ pub fn run(config: &Config) -> Result<()> {
|
||||
let asn = AsnDB::try_open_reader(&cache_dir)?;
|
||||
|
||||
let env = Environment {
|
||||
verbose: 0, // this doesn't do anything since we use a dummy reporter
|
||||
verbose: 0, // this doesn't do anything since we use a dummy ipc child
|
||||
keyring,
|
||||
dns_config,
|
||||
proxy,
|
||||
@@ -101,7 +102,7 @@ pub fn run(config: &Config) -> Result<()> {
|
||||
asn,
|
||||
};
|
||||
|
||||
let tx = DummyReporter::new();
|
||||
let tx = DummyIpcChild::new();
|
||||
let (lua, state) = ctx::ctx(env, tx);
|
||||
let mut repl = Repl::new(lua, state);
|
||||
|
||||
|
||||
30
src/runtime/crypto.rs
Normal file
30
src/runtime/crypto.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use crate::errors::*;
|
||||
|
||||
use crate::engine::ctx::State;
|
||||
use crate::engine::structs::{byte_array, lua_bytes};
|
||||
use crate::hlua::{self, AnyLuaValue};
|
||||
use sn0int_std::crypto;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn key_trunc_pad(lua: &mut hlua::Lua, state: Arc<dyn State>) {
|
||||
lua.set("key_trunc_pad", hlua::function3(move |bytes: AnyLuaValue, len: u32, pad: u8| -> Result<AnyLuaValue> {
|
||||
let bytes = byte_array(bytes)
|
||||
.map_err(|err| state.set_error(err))?;
|
||||
let bytes = crypto::key_trunc_pad(&bytes, len as usize, pad);
|
||||
Ok(lua_bytes(&bytes))
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn sodium_secretbox_open(lua: &mut hlua::Lua, state: Arc<dyn State>) {
|
||||
lua.set("sodium_secretbox_open", hlua::function2(move |encrypted: AnyLuaValue, key: AnyLuaValue| -> Result<AnyLuaValue> {
|
||||
let encrypted = byte_array(encrypted)
|
||||
.map_err(|err| state.set_error(err))?;
|
||||
let key = byte_array(key)
|
||||
.map_err(|err| state.set_error(err))?;
|
||||
|
||||
let plain = crypto::sodium_secretbox_open(&encrypted, &key)
|
||||
.map_err(|err| state.set_error(err))?;
|
||||
|
||||
Ok(lua_bytes(&plain))
|
||||
}))
|
||||
}
|
||||
@@ -177,65 +177,85 @@ fn gen_changeset<T: Model, U: Updateable<T>>(object: LuaJsonValue, mut update: L
|
||||
Ok((existing.id(), value, update))
|
||||
}
|
||||
|
||||
fn gen_changeset2<T: Model, U: UpdateToChangeset<C>, C: Updateable<T>>(object: LuaJsonValue, mut update: LuaJsonValue) -> Result<(i32, String, C)>
|
||||
where
|
||||
for<'de> T: serde::Deserialize<'de>,
|
||||
for<'de> U: serde::Deserialize<'de>,
|
||||
for<'de> C: serde::Deserialize<'de>,
|
||||
{
|
||||
let existing = structs::from_lua::<T>(object)?;
|
||||
|
||||
// copy the id over to the update struct so we can identify the row
|
||||
if let LuaJsonValue::Object(ref mut update) = update {
|
||||
update.insert("id".into(), LuaJsonValue::Number(existing.id().into()));
|
||||
}
|
||||
|
||||
let update = structs::from_lua::<U>(update)?;
|
||||
let mut update = update.try_into_changeset()?;
|
||||
|
||||
let value = existing.to_string();
|
||||
update.changeset(&existing);
|
||||
|
||||
Ok((existing.id(), value, update))
|
||||
}
|
||||
|
||||
fn run_update(state: Arc<dyn State>, family: &str, object: AnyLuaValue, update: AnyLuaValue) -> Result<Option<i32>> {
|
||||
let family = Family::from_str(&family)?;
|
||||
let object = LuaJsonValue::from(object);
|
||||
let update = LuaJsonValue::from(update);
|
||||
|
||||
let (id, value, update) = match family {
|
||||
Family::Domain => bail!("Domain doesn't have mutable fields"),
|
||||
Family::Subdomain => gen_changeset::<Subdomain, SubdomainUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Subdomain(u))),
|
||||
Family::Ipaddr => gen_changeset::<IpAddr, IpAddrUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::IpAddr(u))),
|
||||
Family::SubdomainIpaddr => bail!("Subdomain-IpAddr doesn't have mutable fields"),
|
||||
Family::Url => gen_changeset2::<Url, UrlUpdate, UrlChangeset>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Url(u))),
|
||||
Family::Email => gen_changeset::<Email, EmailUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Email(u))),
|
||||
Family::Phonenumber => gen_changeset::<PhoneNumber, PhoneNumberUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::PhoneNumber(u))),
|
||||
Family::Device => gen_changeset::<Device, DeviceUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Device(u))),
|
||||
Family::Network => gen_changeset::<Network, NetworkUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Network(u))),
|
||||
Family::NetworkDevice => gen_changeset::<NetworkDevice, NetworkDeviceUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::NetworkDevice(u))),
|
||||
Family::Account => gen_changeset::<Account, AccountUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Account(u))),
|
||||
Family::Breach => bail!("Breach doesn't have mutable fields"),
|
||||
Family::BreachEmail => gen_changeset::<BreachEmail, BreachEmailUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::BreachEmail(u))),
|
||||
Family::Image => gen_changeset::<Image, ImageUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Image(u))),
|
||||
Family::Port => gen_changeset::<Port, PortUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Port(u))),
|
||||
Family::Netblock => gen_changeset::<Netblock, NetblockUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Netblock(u))),
|
||||
Family::Cryptoaddr => gen_changeset::<CryptoAddr, CryptoAddrUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::CryptoAddr(u))),
|
||||
}?;
|
||||
|
||||
if update.is_dirty() {
|
||||
let r = match state.db_update(family, value, update)? {
|
||||
// Inserted and Found are technically unreachable
|
||||
DatabaseResponse::Inserted(id) => Some(id),
|
||||
DatabaseResponse::Updated(id) => Some(id),
|
||||
DatabaseResponse::NoChange(id) => Some(id),
|
||||
DatabaseResponse::Found(id) => Some(id),
|
||||
DatabaseResponse::None => None,
|
||||
};
|
||||
Ok(r)
|
||||
} else {
|
||||
Ok(Some(id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn db_update(lua: &mut hlua::Lua, state: Arc<dyn State>) {
|
||||
lua.set("db_update", hlua::function3(move |family: String, object: AnyLuaValue, update: AnyLuaValue| -> Result<Option<i32>> {
|
||||
let family = Family::from_str(&family)
|
||||
.map_err(|e| state.set_error(e.into()))?;
|
||||
let object = LuaJsonValue::from(object);
|
||||
let update = LuaJsonValue::from(update);
|
||||
|
||||
let update = match family {
|
||||
Family::Domain => bail!("Domain doesn't have mutable fields"),
|
||||
Family::Subdomain => gen_changeset::<Subdomain, SubdomainUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Subdomain(u))),
|
||||
Family::Ipaddr => gen_changeset::<IpAddr, IpAddrUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::IpAddr(u))),
|
||||
Family::SubdomainIpaddr => bail!("Subdomain-IpAddr doesn't have mutable fields"),
|
||||
Family::Url => gen_changeset::<Url, UrlUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Url(u))),
|
||||
Family::Email => gen_changeset::<Email, EmailUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Email(u))),
|
||||
Family::Phonenumber => gen_changeset::<PhoneNumber, PhoneNumberUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::PhoneNumber(u))),
|
||||
Family::Device => gen_changeset::<Device, DeviceUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Device(u))),
|
||||
Family::Network => gen_changeset::<Network, NetworkUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Network(u))),
|
||||
Family::NetworkDevice => gen_changeset::<NetworkDevice, NetworkDeviceUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::NetworkDevice(u))),
|
||||
Family::Account => gen_changeset::<Account, AccountUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Account(u))),
|
||||
Family::Breach => bail!("Breach doesn't have mutable fields"),
|
||||
Family::BreachEmail => gen_changeset::<BreachEmail, BreachEmailUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::BreachEmail(u))),
|
||||
Family::Image => gen_changeset::<Image, ImageUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Image(u))),
|
||||
Family::Port => gen_changeset::<Port, PortUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Port(u))),
|
||||
Family::Netblock => gen_changeset::<Netblock, NetblockUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::Netblock(u))),
|
||||
Family::Cryptoaddr => gen_changeset::<CryptoAddr, CryptoAddrUpdate>(object, update)
|
||||
.map(|(id, v, u)| (id, v, Update::CryptoAddr(u))),
|
||||
};
|
||||
|
||||
let (id, value, update) = update
|
||||
.map_err(|e| state.set_error(e))?;
|
||||
|
||||
if update.is_dirty() {
|
||||
let r = state.db_update(value, update)
|
||||
.map_err(|e| state.set_error(e))?;
|
||||
|
||||
let r = match r {
|
||||
// Inserted and Found are technically unreachable
|
||||
DatabaseResponse::Inserted(id) => Some(id),
|
||||
DatabaseResponse::Updated(id) => Some(id),
|
||||
DatabaseResponse::NoChange(id) => Some(id),
|
||||
DatabaseResponse::Found(id) => Some(id),
|
||||
DatabaseResponse::None => None,
|
||||
};
|
||||
Ok(r)
|
||||
} else {
|
||||
Ok(Some(id))
|
||||
}
|
||||
run_update(state.clone(), &family, object, update)
|
||||
.map_err(|e| state.set_error(e))
|
||||
}))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user