Compare commits
43 Commits
0da097168d
...
feat_packa
| Author | SHA1 | Date | |
|---|---|---|---|
| fde7d540ca | |||
| 3e3725ec22 | |||
| 9fa2c02c9d | |||
| cbf41e0842 | |||
| 44b1bcb634 | |||
| ae72957a1a | |||
| 2a530681df | |||
| 7256763c20 | |||
| 54ddafd0fc | |||
| baeab6daf2 | |||
| 5818349d91 | |||
| b0723ae708 | |||
| 4b43bd1338 | |||
| 4fa1e6e4ef | |||
| 06c48f2927 | |||
| 54c0e8a46b | |||
| 7b4dcdd1d8 | |||
| 72d0aa61cd | |||
| e37a84d77d | |||
| 2d6ca2b0bd | |||
| df2b9e7624 | |||
| 46d1270648 | |||
| 5cbf066ccf | |||
| 515e244592 | |||
| fb4322c040 | |||
| f376d8684b | |||
| d0439394cf | |||
| a91632028c | |||
| bccdcf2ca3 | |||
| 18e3a93a38 | |||
| cfb553c975 | |||
| 1466623070 | |||
| c527d40721 | |||
| 781c096abf | |||
| c94fbd4b48 | |||
| 1f00713c1e | |||
| 12cf423550 | |||
| f99726d3b7 | |||
| 13932e572f | |||
| 20120785bd | |||
| 5af4c963ea | |||
| 8d632b0691 | |||
| 6c8399dbff |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,8 +1,7 @@
|
||||
*.gcow2
|
||||
*.qcow2
|
||||
.envrc
|
||||
result
|
||||
example.json
|
||||
go.sum
|
||||
go.mod
|
||||
gokill
|
||||
./gokill
|
||||
output.md
|
||||
thoughts.md
|
||||
|
||||
140
README.md
Normal file
140
README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# gokill
|
||||
|
||||
gokill is aimed at activists, journalists and others that need to protect their data against access under all circumstances.
|
||||
gokill falls under the category of anti-forensic tools, helping you to protect yourself against repression.
|
||||
It is built for worst case scenarios when intruders physical gaining access to a device.
|
||||
In such heated situations gokill helps you automatically executing tasks like:
|
||||
- locking the screen
|
||||
- sending a chat message
|
||||
- deleting data
|
||||
- encrypting partitions
|
||||
- destroying encrypted partitions
|
||||
- and many more
|
||||
|
||||
the tasks gokill executes could be done by hand using shellscripts, cronjobs, daemons ect.
|
||||
but that means everyone needs to figure it out for themselves, and eventually make mistakes.
|
||||
the idea of gokill is to provide a wide variarity of possibilities out of the box while making sure they are well tested.
|
||||
|
||||
gokill aims to be highly configurable and easily extendable.
|
||||
|
||||
'gokill' is a tool that completes some actions when a certain event occurs.
|
||||
actions can vary from shuting down the machine to sending mails over erasing data.
|
||||
actions can be triggert by certain conditions like specific outcomes of unix
|
||||
comands or not having internet connection.
|
||||
|
||||
actions and triggers should be easy to extend and handled like plugins. they
|
||||
also should be self documenting.
|
||||
every action and trigger should be testable at anytime as a 'dry-run'.
|
||||
actions can have a 'stage' defined. the lowest stage is started first,
|
||||
and only when all actions on that stage are finished next stage is triggered
|
||||
|
||||
gokill should run as daemon. config should be read from /etc/somename/config.json
|
||||
|
||||
## Config Example
|
||||
``` json
|
||||
[ //list of triggers
|
||||
{
|
||||
"type": "UsbDisconnect",
|
||||
"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": "sendMail",
|
||||
"options": {
|
||||
"smtpserver": "domain.org",
|
||||
"port": 667,
|
||||
"recipients": [ "mail1@host.org", "mail2@host.org" ],
|
||||
"message": "kill switch was triggered",
|
||||
"attachments": [ "/path/atachments" ],
|
||||
"pubkeys": "/path/to/keys.pub"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
## actions
|
||||
- [x] shutdown
|
||||
- [ ] wipe ram
|
||||
- [ ] send mail
|
||||
- [ ] delete data
|
||||
- [ ] shred area
|
||||
- [x] random 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
|
||||
- [ ] power adapter disconnected
|
||||
- [ ] unix command
|
||||
- anyOf
|
||||
- trigger wrapper containing many triggers and fires as soon as one of them
|
||||
is triggered
|
||||
- allOf
|
||||
- [ ] ipfs trigger
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"unknown.com/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,13 @@ func NewAction(config []internal.ActionConfig) (Action, error) {
|
||||
|
||||
func GetAllActions() []DocumentedAction {
|
||||
return []DocumentedAction{
|
||||
Printer{},
|
||||
TimeOut{},
|
||||
Command{},
|
||||
Printer{},
|
||||
ShellScript{},
|
||||
Shutdown{},
|
||||
SendMatrix{},
|
||||
SendTelegram{},
|
||||
TimeOut{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,27 +2,26 @@ package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"unknown.com/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: "\"\"",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
226
actions/send_matrix.go
Normal file
226
actions/send_matrix.go
Normal 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"
|
||||
|
||||
"unknown.com/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
148
actions/send_telegram.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
//"strconv"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
"unknown.com/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
118
actions/shell_script.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os"
|
||||
|
||||
"unknown.com/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", ""},
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,47 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"encoding/json"
|
||||
|
||||
"unknown.com/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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"unknown.com/gokill/internal"
|
||||
@@ -10,7 +9,7 @@ import (
|
||||
|
||||
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 {
|
||||
|
||||
@@ -4,41 +4,84 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"unknown.com/gokill/internal"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
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)
|
||||
for _, arg := range c.Args {
|
||||
fmt.Printf("%s ", arg)
|
||||
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
|
||||
}
|
||||
|
||||
fmt.Println("")
|
||||
c.ActionChan <- true
|
||||
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) {
|
||||
splitted := strings.Fields(c.Command)
|
||||
|
||||
if len(splitted) == 0 {
|
||||
return "", nil, fmt.Errorf("Command is empty")
|
||||
}
|
||||
|
||||
if len(splitted) == 1 {
|
||||
return splitted[0], []string(nil), nil
|
||||
}
|
||||
|
||||
return splitted[0], splitted[1:], nil
|
||||
}
|
||||
|
||||
func (c Command) Execute() {
|
||||
cmd := exec.Command(c.Command, c.Args...)
|
||||
command, args, err := c.splitCommandString()
|
||||
internal.LogDoc(c).Infof("Executing command: ", c.Command)
|
||||
|
||||
if err != nil {
|
||||
c.ActionChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(command, args...)
|
||||
|
||||
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)
|
||||
@@ -56,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)
|
||||
}
|
||||
|
||||
@@ -65,12 +108,22 @@ func (p Command) GetName() string {
|
||||
}
|
||||
|
||||
func (p Command) GetDescription() string {
|
||||
return "When triggered executes given command"
|
||||
return "Invoces given command using exec."
|
||||
}
|
||||
|
||||
func (p Command) GetExample() string {
|
||||
return `
|
||||
{
|
||||
"type": "Command",
|
||||
"options": {
|
||||
"command": "srm /path/to/file"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func (p Command) GetOptions() []internal.ConfigOption {
|
||||
return []internal.ConfigOption{
|
||||
{"command", "string", "command to execute", ""},
|
||||
{"args", "string[]", "args", ""},
|
||||
}
|
||||
}
|
||||
|
||||
105
cmd/docbuilder/docbuilder.go
Normal file
105
cmd/docbuilder/docbuilder.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"os"
|
||||
"flag"
|
||||
|
||||
"unknown.com/gokill/actions"
|
||||
"unknown.com/gokill/triggers"
|
||||
"unknown.com/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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
BIN
docbuilder
Executable file
BIN
docbuilder
Executable file
Binary file not shown.
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
book/*
|
||||
4
docs/SUMMARY.md
Normal file
4
docs/SUMMARY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Summary
|
||||
|
||||
- [gokill](./README.md)
|
||||
@GOKILL_OPTIONS@
|
||||
21
docs/actions/README.md
Normal file
21
docs/actions/README.md
Normal 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
10
docs/book.toml
Normal 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
33
docs/default.nix
Normal 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
19
docs/triggers/README.md
Normal 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
|
||||
}
|
||||
```
|
||||
18
etc/gokill/config.json
Normal file
18
etc/gokill/config.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
|
||||
"type": "Timeout",
|
||||
"name": "example trigger",
|
||||
"options": {
|
||||
"duration": 5
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "Print",
|
||||
"options": {
|
||||
"message": "hello world"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
42
flake.lock
generated
42
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1697915759,
|
||||
"narHash": "sha256-WyMj5jGcecD+KC8gEs+wFth1J1wjisZf8kVZH13f1Zo=",
|
||||
"lastModified": 1698553279,
|
||||
"narHash": "sha256-T/9P8yBSLcqo/v+FTOBK+0rjzjPMctVymZydbvR/Fak=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "51d906d2341c9e866e48c2efcaac0f2d70bfd43e",
|
||||
"rev": "90e85bc7c1a6fc0760a94ace129d3a1c61c3d035",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
220
flake.nix
220
flake.nix
@@ -4,123 +4,152 @@
|
||||
#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 = ./.;
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
gotools
|
||||
mdbook
|
||||
olm
|
||||
|
||||
postInstall = ''
|
||||
'';
|
||||
dpkg
|
||||
];
|
||||
};
|
||||
|
||||
packages.x86_64-linux.default = self.packages.x86_64-linux.gokill;
|
||||
packages = rec {
|
||||
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 = ''
|
||||
cp -r ./etc $out/ #for .deb packages
|
||||
'';
|
||||
};
|
||||
|
||||
gokill-docbuilder = pkgs.buildGoModule rec {
|
||||
pname = "docbuilder";
|
||||
version = "1.0";
|
||||
vendorHash = currentVendorHash;
|
||||
buildFLags = "-o . $dest/cmd/gokill/docbuilder";
|
||||
src = ./.;
|
||||
|
||||
buildInputs = [
|
||||
pkgs.olm
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
'';
|
||||
};
|
||||
|
||||
gokillSnap = pkgs.snapTools.makeSnap {
|
||||
meta = {
|
||||
name = "gokill";
|
||||
summary = "simple but efficient";
|
||||
description = "this should be longer";
|
||||
architectures = [ "amd64" ];
|
||||
confinement = "classic";
|
||||
apps.gokill.command = "${gokill}/bin/gokill";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.gokill = {
|
||||
description = "gokill daemon";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
docs = pkgs.callPackage (import ./docs/default.nix) { self = self; };
|
||||
|
||||
wantedBy = [ "default.target" ];
|
||||
};
|
||||
default = self.packages.${system}.gokill;
|
||||
|
||||
};
|
||||
|
||||
bundlers.gokillDeb = pkg: pkgs.stdenv.mkDerivation {
|
||||
name = "deb-single-${pkg.name}";
|
||||
buildInputs = [
|
||||
pkgs.fpm
|
||||
];
|
||||
|
||||
unpackPhase = "true";
|
||||
|
||||
buildPhase = ''
|
||||
export HOME=$PWD
|
||||
mkdir -p ./nix/store/
|
||||
for item in "$(cat ${pkgs.referencesByPopularity pkg})"
|
||||
do
|
||||
cp -r $item ./nix/store/
|
||||
done
|
||||
|
||||
mkdir -p ./bin
|
||||
cp -r ${pkg}/bin/* ./bin/
|
||||
|
||||
mkdir -p ./etc
|
||||
cp -r ${pkg}/etc/* ./etc/
|
||||
|
||||
chmod -R a+rwx ./nix
|
||||
chmod -R a+rwx ./bin
|
||||
chmod -R a+rwx ./etc
|
||||
fpm -s dir -t deb --name ${pkg.name} nix bin etc
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -r *.deb $out
|
||||
'';
|
||||
};
|
||||
|
||||
apps = {
|
||||
docs = {
|
||||
type = "app";
|
||||
program = builtins.toString (pkgs.writeScript "docs" ''
|
||||
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."${system}".docs}/share/doc'');
|
||||
};
|
||||
|
||||
exportDEB = {
|
||||
type = "app";
|
||||
program = builtins.toString (pkgs.writeScript "docs" ''
|
||||
${pkgs.nix}/bin/nix bundle --bundler .#bundlers.${system}.gokillDeb .#packages.${system}.gokill'');
|
||||
};
|
||||
};
|
||||
|
||||
})) ({
|
||||
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 = ''
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
'';
|
||||
services.gokill.testRun = false;
|
||||
services.gokill.triggers = [
|
||||
{
|
||||
type = "Timeout";
|
||||
name = "custom timeout";
|
||||
options = {
|
||||
duration = 10;
|
||||
};
|
||||
actions = [
|
||||
];
|
||||
}
|
||||
];
|
||||
users.users.root.password = "root";
|
||||
virtualisation.vmVariant.virtualisation.graphics = false;
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
in
|
||||
nixos.config.system.build.vm;
|
||||
@@ -131,5 +160,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;
|
||||
};
|
||||
}) ;
|
||||
}
|
||||
|
||||
25
go.mod
25
go.mod
@@ -1,3 +1,26 @@
|
||||
module unknown.com/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
51
go.sum
Normal 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=
|
||||
@@ -36,5 +36,6 @@ type ConfigOption struct {
|
||||
type Documenter interface {
|
||||
GetName() string
|
||||
GetDescription() string
|
||||
GetExample() string
|
||||
GetOptions() []ConfigOption
|
||||
}
|
||||
|
||||
63
internal/logging.go
Normal file
63
internal/logging.go
Normal 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
82
nixos-modules/gokill.nix
Normal 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
21
test/lib.nix
Normal 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
40
test/test.nix
Normal 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
|
||||
'';
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package triggers
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"unknown.com/gokill/actions"
|
||||
@@ -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"},
|
||||
|
||||
136
triggers/receive_telegram.go
Normal file
136
triggers/receive_telegram.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package triggers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
"unknown.com/gokill/internal"
|
||||
"unknown.com/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 %s, wanted %s", 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: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package triggers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"unknown.com/gokill/actions"
|
||||
@@ -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 {
|
||||
|
||||
@@ -28,8 +28,9 @@ func NewTrigger(config internal.KillSwitchConfig) (Trigger, error) {
|
||||
|
||||
func GetAllTriggers() []DocumentedTrigger {
|
||||
return []DocumentedTrigger{
|
||||
TimeOut{},
|
||||
EthernetDisconnect{},
|
||||
ReceiveTelegram{},
|
||||
TimeOut{},
|
||||
UsbDisconnect{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package triggers
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -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"},
|
||||
|
||||
Reference in New Issue
Block a user