102 Commits

Author SHA1 Message Date
kpcyrd
1828b67509 Release v0.19.1 2020-06-24 00:46:45 +02:00
kpcyrd
8f2d71e631 Merge pull request #181 from kpcyrd/notify-fix
Fix early exit on notification and make ratelimiter more global
2020-06-23 23:11:59 +02:00
kpcyrd
2277089bda Share the ratelimit state between main modules and all notifications 2020-06-23 05:41:24 +02:00
kpcyrd
63f226f68f Prevent notifications from interfering with ctrl-c register 2020-06-23 04:55:16 +02:00
kpcyrd
da0e6aa482 Release v0.19.0 2020-06-18 21:06:11 +02:00
kpcyrd
82a2bb1bd0 Merge pull request #180 from kpcyrd/exit
Make sn0int more forgiving with accidential ^C
2020-06-18 18:46:29 +02:00
kpcyrd
b96eea97ee Add exit and quit to completion, remove quickstart 2020-06-18 03:38:16 +02:00
kpcyrd
fd420f4107 Make sn0int more forgiving with accidential ^C 2020-06-18 03:38:16 +02:00
kpcyrd
c5a23a5929 Merge pull request #179 from kpcyrd/notify
Trigger events on database insert/update/delete
2020-06-13 16:06:22 +02:00
kpcyrd
eb2e6203be Trigger notification on ttl expire 2020-06-13 00:48:11 +02:00
kpcyrd
b838dc5b31 Refactor db add/update logging code 2020-06-12 23:24:24 +02:00
kpcyrd
983df4a12e Trigger events on database inserts and updates 2020-06-12 21:32:15 +02:00
kpcyrd
9825bad1fe Merge pull request #178 from kpcyrd/notify
Add basic notification system
2020-06-12 04:13:09 +02:00
kpcyrd
0183e5dbcf Document signal notifications 2020-06-12 03:21:46 +02:00
kpcyrd
dacc28e2f1 Document discord notifications 2020-06-12 00:02:22 +02:00
kpcyrd
90b3abc471 Allow access to debug logs in notify scripts 2020-06-11 23:57:53 +02:00
kpcyrd
09333ebd0d Fix timer description 2020-06-11 04:10:11 +02:00
kpcyrd
e8b1b1ac87 Document notifications with pushover 2020-06-11 03:43:51 +02:00
kpcyrd
eb56a66b20 Include example code to write scripts with options 2020-06-11 03:23:37 +02:00
kpcyrd
fbd9909fef Document new notification system 2020-06-11 03:15:24 +02:00
kpcyrd
51e76b4c89 Connect db_activity to notification system 2020-06-11 01:51:59 +02:00
kpcyrd
484a426834 Fix workspace filter 2020-06-10 03:25:40 +02:00
kpcyrd
31b8840f8c Implement glob patterns for topics 2020-06-10 03:11:25 +02:00
kpcyrd
dfc13be9cc Add notify system 2020-06-10 01:53:43 +02:00
kpcyrd
d6bb6a9a1f Merge pull request #176 from kpcyrd/seccomp
Fix seccomp issues on arm
2020-06-09 13:51:39 +02:00
kpcyrd
bfbe8f2c1a Merge pull request #177 from kpcyrd/cal
Implement calendar function
2020-06-09 13:51:31 +02:00
kpcyrd
289b0edbb3 Fix seccomp issues on arm 2020-06-09 01:26:33 +02:00
kpcyrd
b69e5f879b Merge pull request #175 from kpcyrd/bump-deps
Bump dependencies
2020-06-08 14:40:12 +02:00
kpcyrd
d8810a449c Implement an hourly-view 2020-06-08 03:24:26 +02:00
kpcyrd
bdcd052500 Add time view with 12min slices 2020-06-08 03:24:26 +02:00
kpcyrd
26aa17bf4d Fix crash with low activity 2020-06-08 03:24:26 +02:00
kpcyrd
7a6d6cf2d5 Integrate activity into calendar 2020-06-08 03:24:26 +02:00
kpcyrd
e9cdc40821 Implement activity annotation with dummy data 2020-06-08 03:24:26 +02:00
kpcyrd
c4c7b420ce Implement calendar function 2020-06-08 03:24:26 +02:00
kpcyrd
3d304f13bc Bump dependencies 2020-06-08 03:21:22 +02:00
kpcyrd
6ee61c189e Merge pull request #174 from kpcyrd/cargo-deb
Add cargo-deb metadata
2020-06-08 03:19:17 +02:00
kpcyrd
5ad5666caf Merge pull request #173 from kpcyrd/docs
Update install instructions
2020-06-08 01:46:47 +02:00
kpcyrd
e057f8e6be Merge pull request #172 from kpcyrd/seccomp
Fix seccomp issue: clock_nanosleep
2020-06-08 01:45:31 +02:00
kpcyrd
baf44b4882 Add cargo-deb metadata 2020-06-08 01:43:51 +02:00
kpcyrd
f7fda8de4c Fix typo in sandbox docs 2020-06-07 23:59:39 +02:00
kpcyrd
43154cf841 Update install instructions 2020-06-07 23:59:39 +02:00
kpcyrd
49f082875d Update syscallz dependency 2020-06-07 23:13:48 +02:00
kpcyrd
1e575e0dbe Fix seccomp issue: clock_nanosleep 2020-06-07 23:13:48 +02:00
kpcyrd
d741020ff8 Merge pull request #168 from 0x646e78/fedora-install-notes
Add install details for Fedora/RH based systems
2020-06-06 14:33:11 +02:00
DNX
33dca47b38 Add install details for Fedora/RH based systems 2020-05-25 10:20:49 +10:00
kpcyrd
33bd4f9e94 Merge pull request #167 from kpcyrd/agent
Improve `pkg quickstart`, deprecate `mod`
2020-05-09 03:07:35 +02:00
kpcyrd
7a75a7a255 Prevent insertion of domains/subdomains with asterisks 2020-05-05 14:09:34 +02:00
kpcyrd
1c23995c86 Bump dependencies 2020-04-19 15:59:37 +02:00
kpcyrd
23ae7491e3 Make pkg quickstart skip already installed modules 2020-04-01 00:30:07 +02:00
kpcyrd
487578f974 Add deprecation notice for mod command 2020-03-31 23:44:03 +02:00
kpcyrd
4dec06843f Include os version into api useragent 2020-03-24 02:26:52 +01:00
kpcyrd
5f31289430 Release v0.18.2 2020-03-23 19:03:45 +01:00
kpcyrd
b519619324 Fix incomplete osx 10.13 dns bugfix 2020-03-23 19:01:15 +01:00
kpcyrd
5ff78297e4 Release v0.18.1 2020-03-23 15:52:32 +01:00
kpcyrd
c36e0e9a60 Bump dependencies 2020-03-23 15:40:57 +01:00
kpcyrd
6e5a41fa34 Merge pull request #164 from kpcyrd/count
Add select --count
2020-03-23 15:39:58 +01:00
kpcyrd
896e13373e Work around issue with ipv6 dns resolvers 2020-03-21 18:23:53 +01:00
kpcyrd
baeb200a1c Improve error messages 2020-03-20 21:52:09 +01:00
kpcyrd
6648a35f17 Add select --count 2020-03-20 21:52:09 +01:00
kpcyrd
62204e2f31 Merge pull request #162 from kpcyrd/travis
Fix misc issues
2020-03-16 01:50:00 +01:00
kpcyrd
98c1272874 Merge pull request #161 from kpcyrd/oauth
Use new github oauth endpoint
2020-03-16 01:18:13 +01:00
kpcyrd
27df923256 Use new github oauth endpoint 2020-03-16 00:13:35 +01:00
kpcyrd
872d279c49 Support patterns in pkg list 2020-03-15 15:39:41 +01:00
kpcyrd
b548446759 Fix display of netblocks in detailed view 2020-03-15 15:36:18 +01:00
kpcyrd
d5ec02b3d7 Include target triple in sn0int registry requests 2020-03-08 16:25:32 +01:00
kpcyrd
b60de4547f Update travis irc notifications 2020-03-08 15:13:44 +01:00
kpcyrd
7ab4cbd745 Release v0.18.0 2020-03-07 17:30:36 +01:00
kpcyrd
51fa7a02ae Merge pull request #160 from kpcyrd/url-none-bytes
Properly support inserting urls with no body
2020-03-07 16:26:21 +01:00
kpcyrd
39bcc40f55 Document new crypto functions 2020-03-07 15:34:19 +01:00
kpcyrd
38c16d62ee Document mqtt functions 2020-03-07 15:19:02 +01:00
kpcyrd
ecd843c74f docs: extract autonoscope section 2020-03-07 14:59:05 +01:00
kpcyrd
5611d54131 Switch docker container to alpine 2020-03-07 14:49:00 +01:00
kpcyrd
bd5aaaedcd Properly support inserting urls with no body 2020-03-07 03:40:16 +01:00
kpcyrd
c50b770b1e Merge pull request #159 from kpcyrd/mqtt
Add mqtt and libsodium functions
2020-03-05 19:50:45 +01:00
kpcyrd
80f794b521 Fully disable flaky mqtt test 2020-03-05 16:56:17 +01:00
kpcyrd
e22c346537 Allow more direct access to mqtt pkts 2020-03-05 15:51:20 +01:00
kpcyrd
eed2da40b4 Fix flaky test 2020-03-05 10:51:20 +01:00
kpcyrd
b5ba669d10 Do not error for read timeouts in sock_recvline 2020-03-05 03:06:27 +01:00
kpcyrd
832a2608f4 Support geoipupdate path 2020-03-05 03:03:30 +01:00
kpcyrd
ef4b3226ea Add libsodium on osx 2020-03-05 02:29:16 +01:00
kpcyrd
54f2e60695 Add binary support to http_request/http_send 2020-03-05 01:58:45 +01:00
kpcyrd
6f1125516c Add libsodium support for decryption 2020-03-03 16:34:06 +01:00
kpcyrd
76042da044 Add mqtt functions 2020-03-03 01:35:16 +01:00
kpcyrd
193d855f69 Suggest a smaller number of concurrency 2020-02-29 01:39:00 +01:00
kpcyrd
65f282ac4c Update install instructions on sn0int.com 2020-02-29 01:38:24 +01:00
kpcyrd
5fc97140f3 Point to online docs on first start 2020-02-29 01:36:38 +01:00
kpcyrd
0ce8b70f09 Replace quickstart with pkg quickstart 2020-02-29 01:31:01 +01:00
kpcyrd
b4fbca4e0d Merge pull request #158 from kpcyrd/update
Update dependencies
2020-02-28 17:39:05 +01:00
kpcyrd
621bd9304c Document strval and intval 2020-02-28 16:48:00 +01:00
kpcyrd
1ab5972f90 Add more advanced time references 2020-02-28 16:45:36 +01:00
kpcyrd
8aaa5bd167 Update pledge 2020-02-28 15:35:11 +01:00
kpcyrd
4c89888a67 Change update check interval 2020-02-28 15:32:44 +01:00
kpcyrd
fc4d076113 Update x509-parser 2020-02-23 23:37:40 +01:00
kpcyrd
798bd56e75 Release v0.17.1 2020-02-22 19:38:23 +01:00
kpcyrd
5359d1cb95 Merge pull request #157 from kpcyrd/sandbox
seccomp: whitelist ppoll instead of poll on aarch64
2020-02-22 19:34:17 +01:00
kpcyrd
ff7fe3936b Refactor ipc parent/child code 2020-02-22 18:28:33 +01:00
kpcyrd
d1d221f81e Improve child process logging 2020-02-22 17:46:36 +01:00
kpcyrd
e2acdf53a4 Downgrade from broken x509-parser release
https://github.com/rusticata/x509-parser/pull/27
2020-02-22 17:12:09 +01:00
kpcyrd
749d7efab5 Add sn0int run --dump-sandbox-init-msg for debugging 2020-02-22 16:52:36 +01:00
kpcyrd
d96a967fa9 Fix integration test 2020-02-22 16:04:35 +01:00
kpcyrd
3dddd0b041 Add exit and quit commands 2020-02-22 15:19:46 +01:00
kpcyrd
33f02bb832 seccomp: whitelist ppoll instead of poll on aarch64 2020-02-22 15:17:17 +01:00
112 changed files with 5901 additions and 2991 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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]

View File

@@ -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 \

View File

@@ -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)

View File

@@ -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),
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 \

View File

@@ -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
View 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

View File

@@ -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``.

View File

@@ -38,10 +38,12 @@ Getting Started
install
build
usage
autonoscope
scripting
database
structs
activity
notifications
keyring
config
sandbox

View File

@@ -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
View 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

View File

@@ -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
---------

View File

@@ -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

View File

@@ -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

View 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;

View 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;

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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"

View File

@@ -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),

View File

@@ -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" }

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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}}

View File

@@ -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
View 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]);
}
}

View File

@@ -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 {

View File

@@ -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());

View File

@@ -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(),
})
}
}

View File

View File

@@ -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
View 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();
}
*/
}

View File

@@ -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")?;

View File

@@ -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()

View File

@@ -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));
}

View File

@@ -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)?;

View File

@@ -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
View 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
View 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
View 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
}
}

View File

@@ -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
View 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(())
}
}

View File

@@ -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),

View File

@@ -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");

View File

@@ -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
View 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!(),
}
}
}

View File

@@ -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)
},
}
}

View File

@@ -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)
}

View File

@@ -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, &params)?;
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, &params)?;
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, &params)?;
let args = get_args(rl, &module)?;
rl.signal_register().catch_ctrl();
let errors = worker::spawn(rl, &module, args, &params, rl.config().network.proxy.clone(), options);
let errors = worker::spawn(rl, &module, &mut Ratelimiter::new(), args, &params, 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(),

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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");

View File

@@ -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)
}
}

View File

@@ -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(())
}

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,3 @@
pub mod child;
pub mod common;
pub mod parent;

144
src/ipc/parent.rs Normal file
View 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)
}

View File

@@ -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(())
}

View File

@@ -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;

View File

@@ -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),

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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::*;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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
View 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, &params)?;
let args = vec![prepare_arg(&notification)?];
debug!("Executing notification module {:?}", module_name);
let errors = worker::spawn(rl, &module, ratelimit, args, &params, 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"));
}
}

View File

@@ -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)
}
}

View File

@@ -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
View 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))
}))
}

View File

@@ -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