Compare commits

50 Commits

Author SHA1 Message Date
3a4c42f9c0 [readme] fix config example 2023-11-12 01:14:55 +01:00
99529579fe rm binary 2023-11-11 22:07:41 +01:00
49f6a3d26a [triggers/receive_telegram] fix type 2023-11-11 22:04:53 +01:00
60598566d0 [readme] update 2023-11-11 22:04:44 +01:00
15f8793e2f [actions/unix_command] fix typo 2023-11-11 22:04:31 +01:00
d574902919 [actions/remove_files] init 2023-11-11 22:03:26 +01:00
a08de8a553 [go mod] rename module 2023-11-11 15:28:35 +01:00
0b2bba5d77 [gitignore] update && rm binary file 2023-11-11 03:22:10 +01:00
398e16e724 [readme] WIP 2023-11-11 03:20:03 +01:00
e93346f646 [nixpkgs] update 2023-11-11 03:19:41 +01:00
3e3725ec22 [actions/send_telegram] fix chatId type 2023-11-09 14:15:42 +01:00
9fa2c02c9d [triggers/receive_telegram] init 2023-11-09 14:15:31 +01:00
cbf41e0842 [actions/send_telegram] disable debug logs of external lb 2023-11-04 16:54:08 +01:00
44b1bcb634 [actions/send_telegram] fix token 2023-11-03 14:33:01 +01:00
ae72957a1a [docs] update readme 2023-11-03 14:32:52 +01:00
2a530681df [internal/logging] add logging lib 2023-11-03 14:32:44 +01:00
7256763c20 [actions/send_matrix] fix typo 2023-11-02 00:51:15 +01:00
54ddafd0fc [nix] support outputs for different architectures 2023-11-02 00:50:36 +01:00
baeab6daf2 [actions/send_telegram] init 2023-11-02 00:49:06 +01:00
5818349d91 [nix] nixosModule: add testRun option 2023-11-01 11:04:16 +01:00
b0723ae708 [actions/shutdown] fix missing default value 2023-10-31 21:01:54 +01:00
4b43bd1338 [actions/send_matrix] init 2023-10-31 20:40:43 +01:00
4fa1e6e4ef [nix] extract nixos-module/gokill 2023-10-31 16:43:52 +01:00
06c48f2927 [nix] update test to run quicker 2023-10-31 16:43:02 +01:00
54c0e8a46b [nix] add base integration test
run it using 'nix flake check -L'
2023-10-31 15:01:35 +01:00
7b4dcdd1d8 [docs] add minimal nix documentation 2023-10-31 14:47:03 +01:00
72d0aa61cd [docs] update usb 2023-10-31 14:46:45 +01:00
e37a84d77d [triggers/ethernet] fix deprecation warning 2023-10-31 13:47:14 +01:00
2d6ca2b0bd [actions] fix missing return 2023-10-31 13:12:50 +01:00
df2b9e7624 [actions/unix_command] check if commands can be found during test run 2023-10-31 13:12:34 +01:00
46d1270648 [actions/shell_script] init 2023-10-31 13:12:34 +01:00
5cbf066ccf [actions/shutdown] add time option to control delay 2023-10-31 13:12:34 +01:00
515e244592 [docs] fix typo 2023-10-31 02:50:25 +01:00
fb4322c040 [docs] fix typos 2023-10-31 02:47:12 +01:00
f376d8684b [nix] allow configuration of triggers/actions in nix 2023-10-31 02:45:25 +01:00
d0439394cf [docs] include README.md into docs 2023-10-31 00:51:36 +01:00
a91632028c [readme] WIP update 2023-10-31 00:51:36 +01:00
bccdcf2ca3 [docs] add README for triggers and actions 2023-10-31 00:51:36 +01:00
18e3a93a38 [docs] change docbuilder markdown output 2023-10-31 00:51:36 +01:00
cfb553c975 [docs] add Examples to each trigger/action 2023-10-31 00:51:36 +01:00
1466623070 [readme] init 2023-10-31 00:51:36 +01:00
c527d40721 [docs] rm unused file 2023-10-31 00:51:36 +01:00
781c096abf [gitignore] update 2023-10-31 00:51:36 +01:00
c94fbd4b48 [gokill] mv main 2023-10-31 00:51:36 +01:00
1f00713c1e [docs] add docbuilder.go 2023-10-31 00:51:36 +01:00
12cf423550 [gitignore] update 2023-10-31 00:51:36 +01:00
f99726d3b7 [docs] setup flake and dir 2023-10-31 00:51:36 +01:00
13932e572f [docs] init 2023-10-31 00:51:36 +01:00
20120785bd [nix] add devShell 2023-10-30 20:00:43 +01:00
5af4c963ea [actions] handle errors via channel 2023-10-30 19:59:21 +01:00
34 changed files with 1766 additions and 182 deletions

4
.gitignore vendored
View File

@@ -1,8 +1,8 @@
*.qcow2
.envrc
result
example.json
go.sum
go.mod
gokill
output.md
thoughts.md
deb-single*

169
README.md Normal file
View File

@@ -0,0 +1,169 @@
# gokill
gokill is a [software dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch#Software) that empowers users to configure various events. If these events occur, they trigger predefined actions.
The tool is designed for activists, journalists, and individuals who require robust protection for their data, ensuring it remains inaccessible under any circumstances. It belongs to the category of anti-forensic tools, providing a means to safeguard against potential repression. It is specifically crafted for worst-case scenarios, such as when intruders gain physical access to a device. In these intense situations, gokill can automatically perform tasks to enhance your security. Those could be:
- locking the screen
- sending chat messages
- deleting data
- encrypting partitions
- destroying encrypted partitions
- ect
#### documentation
A full list of Triggers and Actions with all their configuration options can be found here:
## usage
If you use NixOS gokill can easily be integrated into your system configuration - scroll down for more info on that.
For all other linux distributions gokill currently needs to be built and setup manually. This is supposed to change.
Iam currently working/researching on publishing gokill as [ppa](https://help.launchpad.net/Packaging/PPA) and as snap.
If you have other recommendations let me know.
``` bash
# Clone the gokill repository
git clone https://github.com/k4lipso/gokill
cd gokill
# Build gokill - requires libolm
go build github.com/k4lipso/gokill
# Create a config.json and run gokill
./gokill -c config.json
# Running gokill manually is annoying, it is acutally meant to run as systemd unit.
```
## Config Example
gokill is configured using a json file. it consists of a list of triggers, where each of the triggers as a list of
actions that will be executed once triggered.
``` json
[ //list of triggers
{
"type": "UsbDisconnect", //triggers when the given device is disconnected
"name": "First Trigger",
"options": {
"deviceId": "ata-Samsung_SSD_860_EVO_1TB_S4AALKWJDI102",
"waitTillConnected": true //only trigger when usb drive was actually attached before
},
"actions": [ //list of actions that will be executed when triggered
{
"name": "unixCommand",
"options": {
"command": "shutdown -h now"
},
"stage": 2 // defines the order in which actions are triggered.
},
{
"type": "SendTelegram",
"options": {
"token": "3345823487:FFGdEFxc1pA18d02Akslw-lkwjdA92KAH2",
"chatId": -832325872,
"message": "attention, intruders got my device!",
"testMessage": "this is just a test, no worries"
},
"stage": 1 //this event is triggered first, then the shutdown
}
]
},
{
"type": "EthernetDisconnect",
"name": "Second Trigger",
"options": {
"interfaceName": "eth0",
},
"actions": [
{
"name": "unixCommand",
"options": {
"command": "env DISPLAY=:0 sudo su -c i3lock someUser" //example of locking someUser's screen as root
}
}
]
}
]
```
## nix support
gokill enjoys full nix support. gokill exposes a nix flakes that outputs a gokill package, a nixosModule and more.
That means you can super easily incorporate gokill into your existing nixosConfigurations.
### NixOS Module
Here is a small example config:
``` nix
{
services.gokill.enable = true;
services.gokill.triggers = [
{
type = "EthernetDisconnect";
name = "MainTrigger";
options = {
interfaceName = "eth1";
};
actions = [
{
type = "Command";
options = {
command = "echo hello world";
};
stage = 1;
}
];
}
];
}
```
This will automatically configure and enable a systemd running gokill as root user in the background
### Build Documentation locally
``` bash
nix run github:k4lipso/gokill#docs
```
### Run integrations tests
``` bash
nix flake check github:k4lipso/gokill
```
## todos
- export snap
- export ppa
### actions
- [x] shutdown
- [ ] wipe ram
- [ ] ~~send mail~~
- send chat message
- [x] telegram
- [x] matrix
- [ ] delete data
- [ ] shred area
- [x] run command
- [ ] wordpress post
- [ ] ipfs command
- [buskill 'triggers'](https://github.com/BusKill/awesome-buskill-triggers)
- [x] [lock-screen](https://github.com/BusKill/buskill-linux/tree/master/triggers)
- [x] shutdown
- [ ] luks header shredder
- [ ] veracrypt self-destruct
### triggers
- [ ] no internet
- [x] [pull usb stick](https://github.com/deepakjois/gousbdrivedetector/blob/master/usbdrivedetector_linux.go)
- [x] ethernet unplugged
- receive specific chat message
- [x] telegram
- [ ] matrix
- [ ] power adapter disconnected
- [ ] unix command
- anyOf
- trigger wrapper containing many triggers and fires as soon as one of them
is triggered
- allOf
- [ ] ipfs trigger

View File

@@ -4,13 +4,15 @@ import (
"fmt"
"sort"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type ActionResultChan chan error
type Action interface {
Execute()
DryExecute()
Create(internal.ActionConfig, chan bool) (Action, error)
Create(internal.ActionConfig, ActionResultChan) (Action, error)
}
type DocumentedAction interface {
@@ -23,7 +25,7 @@ type Stage struct {
}
type StagedActions struct {
ActionChan chan bool
ActionChan ActionResultChan
StageCount int
Stages []Stage
}
@@ -34,13 +36,17 @@ func (a StagedActions) executeInternal(f func(Action)) {
continue
}
fmt.Printf("Execute Stage %v\n", idx+1)
internal.Log.Infof("Execute Stage %v", idx+1)
for actionidx, _ := range stage.Actions {
go f(stage.Actions[actionidx])
}
for range stage.Actions {
<-a.ActionChan
err := <-a.ActionChan
if err != nil {
internal.Log.Errorf("Error occured on Stage %d: %s", idx+1, err)
}
}
}
}
@@ -64,11 +70,11 @@ func (a StagedActions) Execute() {
a.executeInternal(func(a Action) { a.Execute() })
}
func (a StagedActions) Create(config internal.ActionConfig, c chan bool) (Action, error) {
func (a StagedActions) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return StagedActions{}, nil
}
func NewSingleAction(config internal.ActionConfig, c chan bool) (Action, error) {
func NewSingleAction(config internal.ActionConfig, c ActionResultChan) (Action, error) {
for _, availableAction := range GetAllActions() {
if config.Type == availableAction.GetName() {
return availableAction.Create(config, c)
@@ -83,7 +89,7 @@ func NewAction(config []internal.ActionConfig) (Action, error) {
return config[i].Stage < config[j].Stage
})
stagedActions := StagedActions{make(chan bool), 0, []Stage{}}
stagedActions := StagedActions{make(ActionResultChan), 0, []Stage{}}
stageMap := make(map[int][]Action)
@@ -113,10 +119,14 @@ func NewAction(config []internal.ActionConfig) (Action, error) {
func GetAllActions() []DocumentedAction {
return []DocumentedAction{
Printer{},
TimeOut{},
Command{},
Printer{},
RemoveFiles{},
ShellScript{},
Shutdown{},
SendMatrix{},
SendTelegram{},
TimeOut{},
}
}

View File

@@ -2,27 +2,26 @@ package actions
import (
"encoding/json"
"fmt"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type Printer struct {
Message string
ActionChan chan bool
ActionChan ActionResultChan
}
func (p Printer) Execute() {
fmt.Printf("Print action fires. Message: %s\n", p.Message)
p.ActionChan <- true
internal.LogDoc(p).Infof("Print action fires. Message: %s", p.Message)
p.ActionChan <- nil
}
func (p Printer) DryExecute() {
fmt.Printf("Print action fire test. Message: %s\n", p.Message)
p.ActionChan <- true
internal.LogDoc(p).Infof("Print action fire test. Message: %s", p.Message)
p.ActionChan <- nil
}
func (p Printer) Create(config internal.ActionConfig, c chan bool) (Action, error) {
func (p Printer) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
var result Printer
err := json.Unmarshal(config.Options, &result)
@@ -39,11 +38,30 @@ func (p Printer) GetName() string {
}
func (p Printer) GetDescription() string {
return "When triggered prints the configured message to stdout"
return `
Prints a given message to stdout.
This action is mostly used for debugging purposes.
`
}
func (p Printer) GetExample() string {
return `
{
type: "Print",
"options": {
"message": "Hello World!"
}
}
`
}
func (p Printer) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{"message", "string", "Message that should be printed", "\"\""},
{
Name: "message",
Type: "string",
Description: "Message that should be printed",
Default: "\"\"",
},
}
}

130
actions/remove_files.go Normal file
View File

@@ -0,0 +1,130 @@
package actions
import (
"encoding/json"
"fmt"
"os/exec"
"github.com/k4lipso/gokill/internal"
)
type RemoveFiles struct {
Files []string `json:"files"`
Directories []string `json:"directories"`
ActionChan ActionResultChan
}
func (c RemoveFiles) getRemoveCommand() string {
command := "srm"
isAvailable := isCommandAvailable(command)
if !isAvailable {
internal.LogDoc(c).Warningf("Command %s not found, falling back to 'rm'", command)
command = "rm"
}
return command
}
func (c RemoveFiles) DryExecute() {
internal.LogDoc(c).Infof("Test Execute")
command := c.getRemoveCommand()
internal.LogDoc(c).Info("The following commands would have been executed:")
for _, file := range c.Files {
internal.LogDoc(c).Noticef("%s -f %s", command, file)
}
for _, dir := range c.Directories {
internal.LogDoc(c).Noticef("%s -rf %s", command, dir)
}
c.ActionChan <- nil
}
func (c RemoveFiles) Execute() {
internal.LogDoc(c).Infof("Execute")
command := c.getRemoveCommand()
for _, file := range c.Files {
cmd := exec.Command(command, "-fv", file)
stdout, err := cmd.Output()
if err != nil {
internal.LogDoc(c).Errorf("%s", err.Error())
}
internal.LogDoc(c).Notice(string(stdout))
}
for _, dir := range c.Directories {
cmd := exec.Command(command, "-rfv", dir)
stdout, err := cmd.Output()
if err != nil {
internal.LogDoc(c).Errorf("%s", err.Error())
}
internal.LogDoc(c).Notice(string(stdout))
}
c.ActionChan <- nil
}
func CreateRemoveFiles(config internal.ActionConfig, c ActionResultChan) (RemoveFiles, error) {
result := RemoveFiles{}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return RemoveFiles{}, fmt.Errorf("Error parsing RemoveFiles: %s", err)
}
result.ActionChan = c
return result, nil
}
func (cc RemoveFiles) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateRemoveFiles(config, c)
}
func (p RemoveFiles) GetName() string {
return "RemoveFiles"
}
func (p RemoveFiles) GetDescription() string {
return `
RemoveFiles deletes the given files and directories.
If available "srm" is used, otherwise RemoveFiles falls back to "rm"
`
}
func (p RemoveFiles) GetExample() string {
return `
{
"type": "RemoveFiles",
"options": {
"files": [
"/home/user/secrets.txt"
],
"directories": [
"/home/user/.gpg",
"/home/user/.ssh",
"/home/user/.thunderbird"
]
}
}
`
}
func (p RemoveFiles) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{"files", "[]string", "list of absolute paths of files that should be deleted.", ""},
{"directories", "[]string", "list of absolute paths of directories that should be deleted.", ""},
}
}

226
actions/send_matrix.go Normal file
View File

@@ -0,0 +1,226 @@
package actions
import (
"fmt"
"encoding/json"
"context"
"errors"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/crypto/cryptohelper"
"github.com/k4lipso/gokill/internal"
)
type SendMatrix struct {
Homeserver string `json:"homeserver"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
RoomId string `json:"roomId"`
Message string `json:"message"`
TestMessage string `json:"testMessage"`
ActionChan ActionResultChan
}
func (s SendMatrix) sendMessage(message string) error {
client, err := mautrix.NewClient(s.Homeserver, "", "")
if err != nil {
return err
}
cryptoHelper, err := cryptohelper.NewCryptoHelper(client, []byte("meow"), s.Database)
if err != nil {
return err
}
cryptoHelper.LoginAs = &mautrix.ReqLogin{
Type: mautrix.AuthTypePassword,
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: s.Username},
Password: s.Password,
}
err = cryptoHelper.Init()
if err != nil {
return err
}
client.Crypto = cryptoHelper
internal.LogDoc(s).Info("Matrix Client Now running")
syncCtx, cancelSync := context.WithCancel(context.Background())
var syncStopWait sync.WaitGroup
syncStopWait.Add(1)
go func() {
err = client.SyncWithContext(syncCtx)
defer syncStopWait.Done()
if err != nil && !errors.Is(err, context.Canceled) {
return
}
}()
time.Sleep(5 * time.Second)
resp, err := client.SendText(id.RoomID(s.RoomId), message)
if err != nil {
return fmt.Errorf("Failed to send event")
} else {
internal.LogDoc(s).Info("Matrix Client: Message sent")
internal.LogDoc(s).Infof("Matrix Client: event_id: %s", resp.EventID.String())
}
cancelSync()
syncStopWait.Wait()
err = cryptoHelper.Close()
if err != nil {
return fmt.Errorf("Error closing database")
}
return nil
}
func (s SendMatrix) DryExecute() {
internal.LogDoc(s).Info("SendMatrix: Trying to send test message")
err := s.sendMessage(s.TestMessage)
if err != nil {
internal.LogDoc(s).Info("SendMatrix: failed to send test message")
}
s.ActionChan <- err
}
func (s SendMatrix) Execute() {
internal.LogDoc(s).Info("SendMatrix: Trying to send message")
err := s.sendMessage(s.Message)
if err != nil {
internal.LogDoc(s).Info("SendMatrix: failed to send message")
}
s.ActionChan <- err
}
func CreateSendMatrix(config internal.ActionConfig, c ActionResultChan) (SendMatrix, error) {
result := SendMatrix{}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return SendMatrix{}, err
}
if result.Homeserver == "" {
return SendMatrix{}, internal.OptionMissingError{"homeserver"}
}
if result.Username == "" {
return SendMatrix{}, internal.OptionMissingError{"username"}
}
if result.Password == "" {
return SendMatrix{}, internal.OptionMissingError{"password"}
}
if result.Database == "" {
return SendMatrix{}, internal.OptionMissingError{"database"}
}
if result.RoomId == "" {
return SendMatrix{}, internal.OptionMissingError{"roomId"}
}
if result.Message == "" {
return SendMatrix{}, internal.OptionMissingError{"message"}
}
if result.TestMessage == "" {
return SendMatrix{}, internal.OptionMissingError{"testMessage"}
}
result.ActionChan = c
return result, nil
}
func (s SendMatrix) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateSendMatrix(config, c)
}
func (p SendMatrix) GetName() string {
return "SendMatrix"
}
func (p SendMatrix) GetDescription() string {
return "Sends a message to a given room. The user needs to be part of that room already."
}
func (p SendMatrix) GetExample() string {
return `
{
"type": "SendMatrix",
"options": {
"homeserver": "matrix.org",
"username": "testuser",
"password": "super-secret",
"database": "/etc/gokill/matrix.db",
"roomId": "!Balrthajskensaw:matrix.org",
"message": "attention, intruders got my device!",
"testMessage": "this is just a test, no worries"
}
}
`
}
func (p SendMatrix) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "homeserver",
Type: "string",
Description: "homeserver address.",
Default: "",
},
{
Name: "username",
Type: "string",
Description: "username (localpart, wihout homeserver address)",
Default: "",
},
{
Name: "password",
Type: "string",
Description: "password in clear text",
Default: "",
},
{
Name: "database",
Type: "string",
Description: "path to database file, will be created if not existing. this is necessary to sync keys for encryption.",
Default: "",
},
{
Name: "roomId",
Type: "string",
Description: "",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that should be sent",
Default: "",
},
{
Name: "testMessage",
Type: "string",
Description: "message sent during test run",
Default: "",
},
}
}

148
actions/send_telegram.go Normal file
View File

@@ -0,0 +1,148 @@
package actions
import (
"fmt"
"encoding/json"
//"strconv"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/k4lipso/gokill/internal"
)
type SendTelegram struct {
Token string `json:"token"`
ChatId int64 `json:"chatId"`
Message string `json:"message"`
TestMessage string `json:"testMessage"`
ActionChan ActionResultChan
}
func (s SendTelegram) sendMessage(message string) error {
bot, err := tgbotapi.NewBotAPI(s.Token)
if err != nil {
return fmt.Errorf("SendTelegram sendMessage error: %s", err)
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
chatId := s.ChatId
msg := tgbotapi.NewMessage(chatId, message)
_, err = bot.Send(msg)
if err != nil {
return fmt.Errorf("SendTelegram sendMessage error: %s", err)
}
return nil
}
func (s SendTelegram) DryExecute() {
internal.LogDoc(s).Info("SendTelegram: Trying to send test message")
err := s.sendMessage(s.TestMessage)
if err != nil {
internal.LogDoc(s).Info("SendTelegram: failed to send test message")
}
s.ActionChan <- err
}
func (s SendTelegram) Execute() {
internal.LogDoc(s).Info("SendTelegram: Trying to send message")
err := s.sendMessage(s.Message)
if err != nil {
internal.LogDoc(s).Info("SendTelegram: failed to send message")
}
s.ActionChan <- err
}
func CreateSendTelegram(config internal.ActionConfig, c ActionResultChan) (SendTelegram, error) {
result := SendTelegram{
ChatId: 0,
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return SendTelegram{}, err
}
if result.Token == "" {
return SendTelegram{}, internal.OptionMissingError{"token"}
}
if result.ChatId == 0 {
return SendTelegram{}, internal.OptionMissingError{"chatId"}
}
if result.Message == "" {
return SendTelegram{}, internal.OptionMissingError{"message"}
}
if result.TestMessage == "" {
return SendTelegram{}, internal.OptionMissingError{"testMessage"}
}
result.ActionChan = c
return result, nil
}
func (s SendTelegram) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateSendTelegram(config, c)
}
func (p SendTelegram) GetName() string {
return "SendTelegram"
}
func (p SendTelegram) GetDescription() string {
return "Sends a message to a given room. The user needs to be part of that room already."
}
func (p SendTelegram) GetExample() string {
return `
{
"type": "SendTelegram",
"options": {
"token": "5349923487:FFGrETxa0pA29d02Akslw-lkwjdA92KAH2",
"chatId": -832345892,
"message": "attention, intruders got my device!",
"testMessage": "this is just a test, no worries"
}
}
`
}
func (p SendTelegram) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "token",
Type: "string",
Description: "telegram bot token (ask botfather)",
Default: "",
},
{
Name: "chatId",
Type: "int",
Description: "chatId of group or chat you want the message be sent to.",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that should be sent",
Default: "",
},
{
Name: "testMessage",
Type: "string",
Description: "message sent during test run",
Default: "",
},
}
}

118
actions/shell_script.go Normal file
View File

@@ -0,0 +1,118 @@
package actions
import (
"encoding/json"
"fmt"
"os/exec"
"os"
"github.com/k4lipso/gokill/internal"
)
type ShellScript struct {
Path string `json:"path"`
ActionChan ActionResultChan
}
func isExecutableFile(path string) bool {
fi, err := os.Lstat(path)
if err != nil {
return false
}
mode := fi.Mode()
//TODO: should check if current user can execute
if mode&01111 == 0 {
return false
}
return true
}
func (c ShellScript) DryExecute() {
internal.LogDoc(c).Infof("Test Executing ShellScript:\n%s", c.Path)
_, err := os.Open(c.Path)
if err != nil {
internal.LogDoc(c).Warning("Test executing Shellscript Failed.")
c.ActionChan <- err
return
}
if !isExecutableFile(c.Path) {
internal.LogDoc(c).Warning("Test executing Shellscript Failed.")
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
return
}
c.ActionChan <- nil
}
func (c ShellScript) Execute() {
if !isExecutableFile(c.Path) {
internal.LogDoc(c).Warning("Executing Shellscript Failed.")
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
return
}
cmd := exec.Command("/bin/sh", c.Path)
stdout, err := cmd.Output()
if err != nil {
internal.LogDoc(c).Warning("Failed to execute Shellscript")
c.ActionChan <- fmt.Errorf("Error during ShellScript execute: %s", err)
}
internal.LogDoc(c).Infof("Shellscript output:\n%s", string(stdout[:]))
c.ActionChan <- nil
}
func CreateShellScript(config internal.ActionConfig, c ActionResultChan) (ShellScript, error) {
result := ShellScript{}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return ShellScript{}, err
}
if result.Path == "" {
return ShellScript{}, internal.OptionMissingError{"path"}
}
result.ActionChan = c
return result, nil
}
func (cc ShellScript) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateShellScript(config, c)
}
func (p ShellScript) GetName() string {
return "ShellScript"
}
func (p ShellScript) GetDescription() string {
return "Executes the given shell script."
}
func (p ShellScript) GetExample() string {
return `
{
"type": "ShellScript",
"options": {
"path": "/path/to/file.sh"
}
}
`
}
func (p ShellScript) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{"path", "string", "path to script to execute", ""},
}
}

View File

@@ -1,35 +1,47 @@
package actions
import (
"fmt"
"os/exec"
"encoding/json"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type Shutdown struct {
ActionChan chan bool
Timeout string `json:"time"`
ActionChan ActionResultChan
}
func (s Shutdown) DryExecute() {
fmt.Println("Test Shutdown executed...")
s.ActionChan <- true
internal.LogDoc(s).Infof("shutdown -h %s", s.Timeout)
internal.LogDoc(s).Info("Test Shutdown executed...")
s.ActionChan <- nil
}
func (s Shutdown) Execute() {
if err := exec.Command("shutdown", "-h", "now").Run(); err != nil {
fmt.Println("Failed to initiate shutdown:", err)
if err := exec.Command("shutdown", "-h", s.Timeout).Run(); err != nil {
internal.LogDoc(s).Errorf("Failed to initiate shutdown: %s", err)
}
fmt.Println("Shutdown executed...")
internal.LogDoc(s).Notice("Shutdown executed...")
s.ActionChan <- true
s.ActionChan <- nil
}
func (s Shutdown) Create(config internal.ActionConfig, c chan bool) (Action, error) {
return Shutdown{c}, nil
func (s Shutdown) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
result := Shutdown{
Timeout: "now",
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
internal.LogDoc(s).Warning("Parsing Shutdown options failed.")
return Shutdown{}, err
}
result.ActionChan = c
return result, nil
}
func (p Shutdown) GetName() string {
@@ -37,9 +49,27 @@ func (p Shutdown) GetName() string {
}
func (p Shutdown) GetDescription() string {
return "When triggered shuts down the machine"
return "Shutsdown the machine by perfoming a ```shutdown -h now```"
}
func (p Shutdown) GetExample() string {
return `
{
"type": "Shutdown",
"options": {
"time": "+5" //wait 5 minutes before shutdown
}
}
`
}
func (p Shutdown) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{}
return []internal.ConfigOption{
{
Name: "time",
Type: "string",
Description: "TIME parameter passed to shutdown as follows ```shutdown -h TIME```",
Default: "now",
},
}
}

View File

@@ -2,15 +2,14 @@ package actions
import (
"encoding/json"
"fmt"
"time"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type TimeOut struct {
Duration time.Duration
ActionChan chan bool
ActionChan ActionResultChan
}
func (t TimeOut) DryExecute() {
@@ -18,12 +17,12 @@ func (t TimeOut) DryExecute() {
}
func (t TimeOut) Execute() {
fmt.Printf("Waiting %d seconds\n", t.Duration)
internal.LogDoc(t).Infof("Waiting %d seconds", t.Duration)
time.Sleep(time.Duration(t.Duration) * time.Second)
t.ActionChan <- true
t.ActionChan <- nil
}
func (t TimeOut) Create(config internal.ActionConfig, c chan bool) (Action, error) {
func (t TimeOut) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
var result TimeOut
err := json.Unmarshal(config.Options, &result)
@@ -40,7 +39,21 @@ func (p TimeOut) GetName() string {
}
func (p TimeOut) GetDescription() string {
return "When triggered waits given duration before continuing with next stage"
return `
Waits given duration in seconds.
This can be used to wait a certain amount of time before continuing to the next Stage
`
}
func (p TimeOut) GetExample() string {
return `
{
"type": "Timeout",
"options": {
"duration": 5
}
}
`
}
func (p TimeOut) GetOptions() []internal.ConfigOption {

View File

@@ -6,17 +6,42 @@ import (
"os/exec"
"strings"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type Command struct {
Command string `json:"command"`
ActionChan chan bool
ActionChan ActionResultChan
}
func isCommandAvailable(name string) bool {
cmd := exec.Command("/bin/sh", "-c", "command -v "+name)
if err := cmd.Run(); err != nil {
return false
}
return true
}
func (c Command) DryExecute() {
fmt.Printf("Test Executing Command:\n%s ", c.Command)
c.ActionChan <- true
internal.LogDoc(c).Infof("Test Executing Command: %s", c.Command)
command, _, err := c.splitCommandString()
if err != nil {
internal.LogDoc(c).Errorf("Error during argument parsing of command '%s'", c.Command)
c.ActionChan <- fmt.Errorf("Error on Command action: %s", err)
return
}
isAvailable := isCommandAvailable(command)
if !isAvailable {
internal.LogDoc(c).Warningf("Command %s not found", command)
c.ActionChan <- fmt.Errorf("Command %s not found!", command)
return
}
c.ActionChan <- nil
}
func (c Command) splitCommandString() (string, []string, error) {
@@ -35,11 +60,10 @@ func (c Command) splitCommandString() (string, []string, error) {
func (c Command) Execute() {
command, args, err := c.splitCommandString()
fmt.Println("Executing command: ", c.Command)
internal.LogDoc(c).Infof("Executing command: ", c.Command)
if err != nil {
fmt.Println(err)
c.ActionChan <- false
c.ActionChan <- err
return
}
@@ -48,15 +72,16 @@ func (c Command) Execute() {
stdout, err := cmd.Output()
if err != nil {
fmt.Println(err.Error())
internal.LogDoc(c).Errorf("%s", err.Error())
c.ActionChan <- fmt.Errorf("Executing Command '%s' failed: %s", c.Command, err)
}
fmt.Println(string(stdout[:]))
internal.LogDoc(c).Infof("Command Output:\n%s", string(stdout[:]))
c.ActionChan <- true
c.ActionChan <- nil
}
func CreateCommand(config internal.ActionConfig, c chan bool) (Command, error) {
func CreateCommand(config internal.ActionConfig, c ActionResultChan) (Command, error) {
result := Command{}
err := json.Unmarshal(config.Options, &result)
@@ -74,7 +99,7 @@ func CreateCommand(config internal.ActionConfig, c chan bool) (Command, error) {
return result, nil
}
func (cc Command) Create(config internal.ActionConfig, c chan bool) (Action, error) {
func (cc Command) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateCommand(config, c)
}
@@ -83,7 +108,18 @@ func (p Command) GetName() string {
}
func (p Command) GetDescription() string {
return "When triggered executes given command"
return "Invokes given command using exec."
}
func (p Command) GetExample() string {
return `
{
"type": "Command",
"options": {
"command": "srm /path/to/file"
}
}
`
}
func (p Command) GetOptions() []internal.ConfigOption {

View File

@@ -0,0 +1,105 @@
package main
import (
"fmt"
"strings"
"os"
"flag"
"github.com/k4lipso/gokill/actions"
"github.com/k4lipso/gokill/triggers"
"github.com/k4lipso/gokill/internal"
)
func getMarkdown(documenter internal.Documenter) string {
var result string
result += fmt.Sprintf("# %v\n%v\n\n", documenter.GetName(), documenter.GetDescription())
result += fmt.Sprintf("*Example:*\n``` json\n%v\n```\n## Options:\n", documenter.GetExample())
for _, opt := range documenter.GetOptions() {
sanitizedDefault := "\"\""
if len(opt.Default) > 0 {
sanitizedDefault = opt.Default
}
result += fmt.Sprintf("### %v\n%v \n\n*Type:* %v \n\n*Default:* ```%v``` \n",
opt.Name, opt.Description, opt.Type, sanitizedDefault)
}
return result
}
func writeToFile(path string, documenter internal.Documenter) error {
fileName := fmt.Sprintf("%s/%s.md", path, documenter.GetName())
f, err := os.Create(fileName)
if err != nil {
internal.Log.Errorf("Error during writeToFile: %s", err)
return err
}
defer f.Close()
_, err = f.WriteString(getMarkdown(documenter))
if err != nil {
internal.Log.Errorf("Error during writeToFile: %s", err)
return err
}
return nil
}
func writeDocumentersToFiles(destination string) {
writeFolder := func(typeName string, documenters []internal.Documenter) {
path := fmt.Sprintf("%s/%s", destination, typeName)
_ = os.Mkdir(path, os.ModePerm)
for _, documenter := range documenters {
writeToFile(path, documenter)
}
}
actions := actions.GetDocumenters()
writeFolder("actions", actions)
triggers := triggers.GetDocumenters()
writeFolder("triggers", triggers)
}
func printDocumentersSummary() {
result := fmt.Sprintf("- [Triggers](triggers/README.md)\n")
for _, trigger := range triggers.GetDocumenters() {
result += fmt.Sprintf("\t- [%s](triggers/%s.md)\n", trigger.GetName(), trigger.GetName())
}
result += fmt.Sprintf("- [Actions](actions/README.md)\n")
for _, action := range actions.GetDocumenters() {
result += fmt.Sprintf("\t- [%s](actions/%s.md)\n", action.GetName(), action.GetName())
}
fmt.Print(result)
}
func main() {
outputPath := flag.String("output", "", "path where docs/ shoud be created")
verbose := flag.Bool("verbose", false, "log debug information")
flag.Parse()
internal.InitLogger()
internal.SetVerbose(*verbose)
if *outputPath == "" {
printDocumentersSummary()
return
}
if len(*outputPath) > 1 {
*outputPath = strings.TrimSuffix(*outputPath, "/")
}
writeDocumentersToFiles(*outputPath)
}

View File

@@ -6,9 +6,9 @@ import (
"fmt"
"os"
"unknown.com/gokill/actions"
"unknown.com/gokill/internal"
"unknown.com/gokill/triggers"
"github.com/k4lipso/gokill/actions"
"github.com/k4lipso/gokill/internal"
"github.com/k4lipso/gokill/triggers"
)
func GetDocumentation() string {
@@ -37,19 +37,24 @@ func GetDocumentation() string {
}
func main() {
configFilePath := flag.String("c", "", "path to config file")
showDoc := flag.Bool("d", false, "show doc")
testRun := flag.Bool("t", false, "test run")
verbose := flag.Bool("verbose", false, "log debug info")
flag.Parse()
internal.InitLogger()
internal.SetVerbose(*verbose)
if *showDoc {
fmt.Print(GetDocumentation())
return
}
if *configFilePath == "" {
fmt.Println("No config file given. Use --help to show usage.")
internal.Log.Warning("No config file given. Use --help to show usage.")
return
}
@@ -58,7 +63,7 @@ func main() {
configFile, err := os.ReadFile(*configFilePath)
if err != nil {
fmt.Println("Error loading config file: ", err)
internal.Log.Errorf("Error loading config file: %s", err)
return
}
@@ -66,7 +71,7 @@ func main() {
err = json.Unmarshal(configFile, &f)
if err != nil {
fmt.Println(err)
internal.Log.Errorf("Error pasing json file: %s", err)
return
}
@@ -75,7 +80,7 @@ func main() {
trigger, err := triggers.NewTrigger(cfg)
if err != nil {
fmt.Println(err)
internal.Log.Errorf("%s", err)
return
}

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
book/*

4
docs/SUMMARY.md Normal file
View File

@@ -0,0 +1,4 @@
# Summary
- [gokill](./README.md)
@GOKILL_OPTIONS@

21
docs/actions/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Actions
Actions are executed when their parent Trigger got triggered.
They then perform some certain task depending on the specific action.
Those can vary from shutding down the machine, removing a file or running a bash command.
**Some Actions may cause permanent damage to the system. This is intended but should be used with caution.**
Actions can have a ```Stage``` assigned to define in which order they should run.
The lowest stage is executed first and only when finished the next stage is executed.
Actions on the same Stage run concurrently.
Actions have the following syntax:
``` json
{
"type": "SomeAction",
"options": { //each action defines its own options
"firstOption": "someValue",
"stage": 2 //this (positive) number defines the order of multiple actions
}
}
```

10
docs/book.toml Normal file
View File

@@ -0,0 +1,10 @@
[book]
authors = []
language = "en"
multilingual = false
src = "."
title = "gokill docs"
[output.html.fold]
enable = true
level = 0

33
docs/default.nix Normal file
View File

@@ -0,0 +1,33 @@
{ pkgs, lib, self, ... }:
with lib;
let
docbuilder = self.packages.x86_64-linux.gokill-docbuilder;
prepareMD = ''
# Copy inputs into the build directory
cp -r --no-preserve=all $inputs/* ./
cp ${../README.md} ./README.md
${docbuilder}/bin/docbuilder --output ./
substituteInPlace ./SUMMARY.md \
--replace "@GOKILL_OPTIONS@" "$(${docbuilder}/bin/docbuilder)"
cat ./SUMMARY.md
'';
in
pkgs.stdenv.mkDerivation {
name = "gokill-docs";
phases = [ "buildPhase" ];
buildInputs = [ pkgs.mdbook ];
inputs = sourceFilesBySuffices ./. [ ".md" ".toml" ];
buildPhase = ''
dest=$out/share/doc
mkdir -p $dest
${prepareMD}
mdbook build
cp -r ./book/* $dest
'';
}

19
docs/triggers/README.md Normal file
View File

@@ -0,0 +1,19 @@
# Triggers
Triggers wait for certain events and execute the actions defined for them.
There are different Triggers for different use cases.
For example ```UsbDisconnect``` is triggered when a certain Usb Drive is unplugged.
If you want your actions to be triggered when an ethernet cable is pulled use ```EthernetDisconnect``` instead.
Triggers have the following syntax:
``` json
{
"type": "SomeTrigger",
"name": "MyFirstTrigger",
"options": { //each trigger defines its own options
"firstOption": 23,
"secondOption": "foo"
},
"actions": [] //list actions that should be executed here
}
```

42
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1697915759,
"narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=",
"lastModified": 1699343069,
"narHash": "sha256-s7BBhyLA6MI6FuJgs4F/SgpntHBzz40/qV0xLPW6A1Q=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e",
"rev": "ec750fd01963ab6b20ee1f0cb488754e8036d89d",
"type": "github"
},
"original": {
@@ -18,7 +18,41 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},

172
flake.nix
View File

@@ -4,118 +4,101 @@
#nixpkgs for testing framework
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, ... }:
inputs.utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, utils, ... }:
nixpkgs.lib.attrsets.recursiveUpdate
(utils.lib.eachSystem (utils.lib.defaultSystems) ( system:
let
forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" ];
pkgs = nixpkgs.legacyPackages.${system};
currentVendorHash = "sha256-Q14p7L2Ez/kvBhMUxlyMA1I/XEIxgSXOp4dpmH/SQyI=";
in
{
packages.x86_64-linux.gokill = nixpkgs.legacyPackages.x86_64-linux.buildGoModule rec {
pname = "gokill";
version = "1.0";
vendorHash = "sha256-aKEOMeW9QVSLsSuHV4b1khqM0rRrMjJ6Eu5RjY+6V8k=";
src = ./.;
postInstall = ''
'';
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go
gotools
mdbook
olm
];
};
packages.x86_64-linux.default = self.packages.x86_64-linux.gokill;
packages = {
gokill = pkgs.buildGoModule rec {
pname = "gokill";
version = "1.0";
vendorHash = currentVendorHash;
src = ./.;
nixosModules.gokill = { config, lib, pkgs, ... }:
let
cfg = config.services.gokill;
configFile = pkgs.writeText "config.json" ''${cfg.extraConfig}'';
gokill-pkg = self.packages.x86_64-linux.gokill;
in
{
options = {
services.gokill = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = lib.mdDoc ''
Enables gokill daemon
'';
};
buildInputs = [
pkgs.olm
];
extraConfig = lib.mkOption {
type = lib.types.str;
description = lib.mdDoc ''
gokill config.json
'';
};
};
postInstall = ''
'';
};
config = lib.mkIf cfg.enable {
systemd.services.gokill = {
description = "gokill daemon";
serviceConfig = {
Type = "simple";
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
Restart = "on-failure";
};
gokill-docbuilder = pkgs.buildGoModule rec {
pname = "docbuilder";
version = "1.0";
vendorHash = currentVendorHash;
buildFLags = "-o . $dest/cmd/gokill/docbuilder";
src = ./.;
wantedBy = [ "default.target" ];
};
buildInputs = [
pkgs.olm
];
postInstall = ''
'';
};
docs = pkgs.callPackage (import ./docs/default.nix) { self = self; };
default = self.packages.${system}.gokill;
};
apps = {
docs = {
type = "app";
program = builtins.toString (pkgs.writeScript "docs" ''
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."${system}".docs}/share/doc'');
};
};
})) ({
nixosModules.gokill = import ./nixos-modules/gokill.nix { self = self; };
packages.x86_64-linux.testVm =
let
nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = [
self.nixosModules.gokill
{
services.gokill.enable = true;
services.gokill.extraConfig = ''
[
services.gokill.testRun = false;
services.gokill.triggers = [
{
type = "Timeout";
name = "custom timeout";
options = {
duration = 10;
};
actions = [
{
"type": "Timeout",
"name": "custom timeout",
"options": {
"duration": 30
},
"actions": [
{
"type": "Print",
"options": {
"message": "Stage 1 triggered. Waiting 25 seconds"
},
"stage": 1
},
{
"type": "Timeout",
"options": {
"duration": 20
},
"stage": 1
},
{
"type": "Timeout",
"options": {
"duration": 5
},
"stage": 2
},
{
"type": "Print",
"options": {
"message": "Shutdown in 5 seconds..."
},
"stage": 2
},
{
"type": "Shutdown",
"options": {
},
"stage": 3
}
]
type = "Shutdown";
options = {
};
stage = 2;
}
]
'';
];
}
];
users.users.root.password = "root";
virtualisation.vmVariant.virtualisation.graphics = false;
}
@@ -131,5 +114,14 @@
${self.packages."x86_64-linux".testVm}/bin/run-nixos-vm
'');
};
};
checks.x86_64-linux = let
checkArgs = {
pkgs = nixpkgs.legacyPackages."x86_64-linux";
inherit self;
};
in {
gokill = import ./test/test.nix checkArgs;
};
}) ;
}

27
go.mod
View File

@@ -1,3 +1,26 @@
module unknown.com/gokill
module github.com/k4lipso/gokill
go 1.20
go 1.21.3
require (
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/mattn/go-sqlite3 v1.14.17
maunium.net/go/mautrix v0.16.1
)
require (
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/rs/zerolog v1.30.0 // indirect
github.com/tidwall/gjson v1.16.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
go.mau.fi/util v0.1.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
maunium.net/go/maulogger/v2 v2.4.1 // indirect
)

51
go.sum Normal file
View File

@@ -0,0 +1,51 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6 h1:qISSdUEX4sjDHfdD/vf65fhuCh3pIhiILDB7ktjJrqU=
github.com/apsdehal/go-logger v0.0.0-20190515212710-b0d6ccfee0e6/go.mod h1:U3/8D6R9+bVpX0ORZjV+3mU9pQ86m7h1lESgJbXNvXA=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
go.mau.fi/util v0.1.0 h1:BwIFWIOEeO7lsiI2eWKFkWTfc5yQmoe+0FYyOFVyaoE=
go.mau.fi/util v0.1.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.16.1 h1:Wb3CvOCe8A/NLsFeZYxKrgXKiqeZUQEBD1zqm7n/kWk=
maunium.net/go/mautrix v0.16.1/go.mod h1:2Jf15tulVtr6LxoiRL4smRXwpkGWUNfBFhwh/aXDBuk=

View File

@@ -36,5 +36,6 @@ type ConfigOption struct {
type Documenter interface {
GetName() string
GetDescription() string
GetExample() string
GetOptions() []ConfigOption
}

63
internal/logging.go Normal file
View File

@@ -0,0 +1,63 @@
package internal
import (
"github.com/apsdehal/go-logger"
"fmt"
"os"
)
var Log *logger.Logger
var documenterLoggers map[string]*logger.Logger
var verbose bool
var formatNormal string = "%{message}"
var formatVerbose string = "%{time} %{level}: %{message}"
var formatModuleNormal string = "%{module}: %{message}"
var formatModuleVerbose string = "%{time} %{level} %{module}: %{message}"
func InitLogger() error {
var err error
Log, err = logger.New("UsbDisconnect", 1, os.Stdout)
if err != nil {
return fmt.Errorf("Logger initialization error: %s", err)
}
documenterLoggers = make(map[string]*logger.Logger)
verbose = false
Log.SetLogLevel(logger.InfoLevel)
return nil
}
func LogDoc(documenter Documenter) *logger.Logger {
documenterLogger, ok := documenterLoggers[documenter.GetName()]
if !ok {
documenterLogger, _ = logger.New(documenter.GetName(), 1, os.Stdout)
documenterLoggers[documenter.GetName()] = documenterLogger
}
SetVerbose(verbose)
//documenterLogger.SetFormat("%{message}")
return documenterLogger
}
func SetVerbose(value bool) {
verbose = value
toggleVerbosity := func(l *logger.Logger, formatN string, formatV string) {
if value {
l.SetLogLevel(logger.DebugLevel)
l.SetFormat(formatV)
} else {
l.SetLogLevel(logger.InfoLevel)
l.SetFormat(formatN)
}
}
toggleVerbosity(Log, formatNormal, formatVerbose)
for _, documenterLogger := range documenterLoggers {
toggleVerbosity(documenterLogger, formatModuleNormal, formatModuleVerbose)
}
}

82
nixos-modules/gokill.nix Normal file
View File

@@ -0,0 +1,82 @@
flake: { config, lib, pkgs, self, ... }:
let
cfg = config.services.gokill;
configFile = pkgs.writeText "config.json" (builtins.toJSON cfg.triggers);
gokill-pkg = self.packages.x86_64-linux.gokill;
testRun = if cfg.testRun then "-t" else "";
in
{
options = with lib; {
services.gokill = {
enable = mkOption {
default = false;
type = types.bool;
description = mdDoc ''
Enables gokill daemon
'';
};
testRun = mkOption {
default = false;
type = types.bool;
description = mdDoc ''
When set to true gokill is performing a test run
'';
};
triggers = mkOption {
description = "list of triggers";
default = [];
type = with types; types.listOf ( submodule {
options = {
type = mkOption {
type = types.str;
};
name = mkOption {
type = types.str;
};
options = mkOption {
type = types.attrs;
};
actions = mkOption {
description = "list of actions";
type = with types; types.listOf ( submodule {
options = {
type = mkOption {
type = types.str;
};
options = mkOption {
type = types.attrs;
};
stage = mkOption {
type = types.int;
};
};
});
};
};
});
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.gokill = {
description = "gokill daemon";
serviceConfig = {
Type = "simple";
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile} ${testRun}";
Restart = "on-failure";
};
wantedBy = [ "default.target" ];
};
};
}

21
test/lib.nix Normal file
View File

@@ -0,0 +1,21 @@
# tests/lib.nix
# based on https://blog.thalheim.io/2023/01/08/how-to-use-nixos-testing-framework-with-flakes/
# The first argument to this function is the test module itself
test:
# These arguments are provided by `flake.nix` on import, see checkArgs
{ pkgs, self}:
let
inherit (pkgs) lib;
# this imports the nixos library that contains our testing framework
nixos-lib = import (pkgs.path + "/nixos/lib") {};
in
(nixos-lib.runTest {
hostPkgs = pkgs;
# This speeds up the evaluation by skipping evaluating documentation (optional)
defaults.documentation.enable = lib.mkDefault false;
# This makes `self` available in the NixOS configuration of our virtual machines.
# This is useful for referencing modules or packages from your own flake
# as well as importing from other flakes.
node.specialArgs = { inherit self; };
imports = [ test ];
}).config.result

40
test/test.nix Normal file
View File

@@ -0,0 +1,40 @@
(import ./lib.nix) {
name = "gokill-base-test";
nodes = {
node1 = { self, pkgs, ... }: {
imports = [ self.nixosModules.gokill ];
services.gokill = {
enable = true;
triggers = [
{
type = "Timeout";
name = "custom timeout";
options = {
duration = 3;
};
actions = [
{
type = "Command";
options = {
command = "echo hello world";
};
stage = 2;
}
];
}
];
};
};
};
testScript = ''
import time
start_all() # wait for our service to start
node1.wait_for_unit("gokill")
time.sleep(4)
output = node1.succeed("journalctl -u gokill.service | tail -n 2 | head -n 1")
# Check if our webserver returns the expected result
assert "hello world" in output
'';
}

View File

@@ -3,11 +3,11 @@ package triggers
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"time"
"unknown.com/gokill/actions"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/actions"
"github.com/k4lipso/gokill/internal"
)
type EthernetDisconnect struct {
@@ -17,10 +17,10 @@ type EthernetDisconnect struct {
}
func isEthernetConnected(deviceName string) bool {
content, err := ioutil.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", deviceName))
content, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", deviceName))
if err != nil {
fmt.Println(err)
internal.LogDoc(EthernetDisconnect{}).Errorf("Cant read devices operstate. Check the deviceName. error: %s", err)
return false
}
@@ -87,6 +87,21 @@ func (p EthernetDisconnect) GetDescription() string {
return "Triggers if Ethernetcable is disconnected."
}
func (p EthernetDisconnect) GetExample() string {
return `
{
"type": "EthernetDisconnect",
"name": "Example Trigger",
"options": {
"interfaceName": "eth0",
"waitTillConnected": true
},
"actions": [
]
}
`
}
func (p EthernetDisconnect) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{"waitTillConnected", "bool", "Only trigger when device was connected before", "true"},

View File

@@ -3,7 +3,7 @@ package triggers
import (
"testing"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
func TestEthernetDisconnetConfig(t *testing.T) {

View File

@@ -0,0 +1,135 @@
package triggers
import (
"fmt"
"encoding/json"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/k4lipso/gokill/internal"
"github.com/k4lipso/gokill/actions"
)
type ReceiveTelegram struct {
Token string `json:"token"`
ChatId int64 `json:"chatId"`
Message string `json:"message"`
action actions.Action
}
func (s ReceiveTelegram) Listen() {
bot, err := tgbotapi.NewBotAPI(s.Token)
if err != nil {
return //fmt.Errorf("ReceiveTelegram waitForMessage error: %s", err)
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
msg := tgbotapi.NewMessage(-746157642, "BOT TEST MESSAGE")
bot.Send(msg)
chatId := s.ChatId
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil { // If we got a message
if(update.Message.Chat.ID != chatId) {
internal.LogDoc(s).Debugf("ReceiveTelegram received wrong ChatId. Got %d, wanted %d", update.Message.Chat.ID, s.ChatId)
continue
}
if(update.Message.Text != s.Message) {
internal.LogDoc(s).Debug("ReceiveTelegram received wrong Message")
continue
}
internal.LogDoc(s).Info("ReceiveTelegram received secret message")
actions.Fire(s.action)
}
}
}
func CreateReceiveTelegram(config internal.KillSwitchConfig) (ReceiveTelegram, error) {
result := ReceiveTelegram{
ChatId: 0,
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return ReceiveTelegram{}, fmt.Errorf("Error during CreateReceiveTelegram: %s", err)
}
if result.Token == "" {
return ReceiveTelegram{}, internal.OptionMissingError{"token"}
}
if result.ChatId == 0 {
return ReceiveTelegram{}, internal.OptionMissingError{"chadId"}
}
if result.Message == "" {
return ReceiveTelegram{}, internal.OptionMissingError{"message"}
}
action, err := actions.NewAction(config.Actions)
if err != nil {
return ReceiveTelegram{}, fmt.Errorf("Error during CreateReceiveTelegram: %s", err)
}
result.action = action
return result, nil
}
func (e ReceiveTelegram) Create(config internal.KillSwitchConfig) (Trigger, error) {
return CreateReceiveTelegram(config)
}
func (p ReceiveTelegram) GetName() string {
return "ReceiveTelegram"
}
func (p ReceiveTelegram) GetDescription() string {
return "Waits for a specific message in a given chat. Once the message is received, the trigger fires."
}
func (p ReceiveTelegram) GetExample() string {
return `
{
"type": "ReceiveTelegram",
"options": {
"token": "5349923487:FFGrETxa0pA29d02Akslw-lkwjdA92KAH2",
"chatId": -832345892,
"message": "secretmessagethatfiresthetrigger"
}
}
`
}
func (p ReceiveTelegram) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{
Name: "token",
Type: "string",
Description: "telegram bot token (ask botfather)",
Default: "",
},
{
Name: "chatId",
Type: "int",
Description: "chatId of group or chat you want the message be received from.",
Default: "",
},
{
Name: "message",
Type: "string",
Description: "actual message that, when received, fires the trigger",
Default: "",
},
}
}

View File

@@ -1,12 +1,10 @@
package triggers
import (
"encoding/json"
"fmt"
"time"
"unknown.com/gokill/actions"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/actions"
"github.com/k4lipso/gokill/internal"
)
type TimeOut struct {
@@ -15,10 +13,10 @@ type TimeOut struct {
}
func (t TimeOut) Listen() {
fmt.Println("TimeOut listens")
fmt.Println(t.Duration)
internal.LogDoc(t).Info("TimeOut listens")
internal.LogDoc(t).Infof("%d", t.Duration)
time.Sleep(time.Duration(t.Duration) * time.Second)
fmt.Println("TimeOut fires")
internal.LogDoc(t).Notice("TimeOut fires")
actions.Fire(t.action)
}
@@ -45,7 +43,21 @@ func (p TimeOut) GetName() string {
}
func (p TimeOut) GetDescription() string {
return "Triggers after given duration."
return "Triggers after given duration. Mostly used for debugging."
}
func (p TimeOut) GetExample() string {
return `
{
"type": "Timeout",
"name": "Example Trigger",
"options": {
"duration": 5
},
"actions": [
]
}
`
}
func (p TimeOut) GetOptions() []internal.ConfigOption {

View File

@@ -3,7 +3,7 @@ package triggers
import (
"fmt"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/internal"
)
type Trigger interface {
@@ -28,8 +28,9 @@ func NewTrigger(config internal.KillSwitchConfig) (Trigger, error) {
func GetAllTriggers() []DocumentedTrigger {
return []DocumentedTrigger{
TimeOut{},
EthernetDisconnect{},
ReceiveTelegram{},
TimeOut{},
UsbDisconnect{},
}
}

View File

@@ -3,12 +3,11 @@ package triggers
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
"unknown.com/gokill/actions"
"unknown.com/gokill/internal"
"github.com/k4lipso/gokill/actions"
"github.com/k4lipso/gokill/internal"
)
type UsbDisconnect struct {
@@ -35,8 +34,8 @@ func (t UsbDisconnect) Listen() {
time.Sleep(1 * time.Second)
}
fmt.Printf("Device %s detected.\n", t.DeviceName)
fmt.Println("UsbDisconnect Trigger is Armed")
internal.LogDoc(t).Infof("Device %s detected.", t.DeviceName)
internal.LogDoc(t).Notice("Trigger is Armed")
}
for {
@@ -85,9 +84,28 @@ func (p UsbDisconnect) GetName() string {
}
func (p UsbDisconnect) GetDescription() string {
return "Triggers when given usb drive is disconnected"
return `
Triggers when given usb drive is disconnected.
Currently it simply checks that the file /dev/disk/by-id/$deviceId exists.
`
}
func (p UsbDisconnect) GetExample() string {
return `
{
"type": "UsbDisconnect",
"name": "Example Trigger",
"options": {
"deviceId": "ata-Samsung_SSD_860_EVO_1TB_S4AALKWJDI102",
"waitTillConnected": true
},
"actions": [
]
}
`
}
func (p UsbDisconnect) GetOptions() []internal.ConfigOption {
return []internal.ConfigOption{
{"waitTillConnected", "bool", "Only trigger when device was connected before", "true"},