Compare commits

29 Commits

Author SHA1 Message Date
c0111f2720 [docs] include README.md into docs 2023-10-31 00:46:57 +01:00
bcaacc1634 [readme] WIP update 2023-10-31 00:46:10 +01:00
abbd1561f2 [docs] add README for triggers and actions 2023-10-31 00:45:44 +01:00
b9b7c0bf3b [docs] change docbuilder markdown output 2023-10-31 00:44:49 +01:00
8898565ff8 [docs] add Examples to each trigger/action 2023-10-31 00:41:59 +01:00
06534e0bbd [readme] init 2023-10-30 23:08:02 +01:00
d4a660383e [docs] rm unused file 2023-10-30 23:00:32 +01:00
e96bbb5f49 [gitignore] update 2023-10-30 23:00:22 +01:00
ad9060c8f6 [gokill] mv main 2023-10-30 23:00:05 +01:00
58946000e1 [docs] add docbuilder.go 2023-10-30 22:57:34 +01:00
b08446bbff [gitignore] update 2023-10-30 22:57:34 +01:00
6c750a947f [docs] setup flake and dir 2023-10-30 22:57:34 +01:00
b2e20c5d75 [nix] add devShell 2023-10-30 22:57:34 +01:00
737b90d597 [actions] handle errors via channel 2023-10-30 22:57:34 +01:00
e9969abf1c [docs] init 2023-10-30 19:34:16 +01:00
8d632b0691 [actions/unix_command] rm args param to simplify usage 2023-10-30 15:07:43 +01:00
6c8399dbff [gitignore] update 2023-10-30 15:07:26 +01:00
0da097168d update gitignore 2023-10-28 14:39:47 +02:00
401b0a5c23 [nix] flake init 2023-10-28 14:35:42 +02:00
8e8088ab39 [triggers/usb] fix print statement 2023-10-28 12:37:37 +02:00
2b33525ea1 [actions/unix_command] init 2023-10-28 12:37:18 +02:00
d6f09d7c84 [triggers/usb] add UsbDisconnect Trigger 2023-10-28 11:40:17 +02:00
cec34477c0 [actions/printer] add missing newline 2023-10-28 11:39:05 +02:00
15bb4dc862 fix timeout 2023-10-25 01:09:02 +02:00
2df2a00721 fix ethernet_test 2023-10-24 23:35:30 +02:00
0354d86796 refactor triggger/action registration 2023-09-30 18:49:12 +02:00
64bce5827c add gitignore 2023-09-30 18:48:42 +02:00
74a61c0a51 change doc output 2023-09-30 15:32:02 +02:00
62aff64f3b add basic ethernet test 2023-09-01 23:17:29 +02:00
24 changed files with 942 additions and 57 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*.qcow2
.envrc
result
example.json
go.sum
go.mod
./gokill
output.md
thoughts.md

90
README.md Normal file
View File

@@ -0,0 +1,90 @@
# gokill
'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"
}
}
]
}
]
```
## 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

View File

@@ -7,9 +7,17 @@ import (
"unknown.com/gokill/internal"
)
type ActionResultChan chan error
type Action interface {
Execute()
DryExecute()
Create(internal.ActionConfig, ActionResultChan) (Action, error)
}
type DocumentedAction interface {
Action
internal.Documenter
}
type Stage struct {
@@ -17,7 +25,7 @@ type Stage struct {
}
type StagedActions struct {
ActionChan chan bool
ActionChan ActionResultChan
StageCount int
Stages []Stage
}
@@ -34,7 +42,11 @@ func (a StagedActions) executeInternal(f func(Action)) {
}
for range stage.Actions {
<-a.ActionChan
err := <-a.ActionChan
if err != nil {
fmt.Printf("Error occured on Stage %d: %s", idx+1, err)
}
}
}
}
@@ -58,21 +70,15 @@ func (a StagedActions) Execute() {
a.executeInternal(func(a Action) { a.Execute() })
}
func NewSingleAction(config internal.ActionConfig, c chan bool) (Action, error) {
if config.Type == "Print" {
return NewPrint(config, c)
}
func (a StagedActions) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return StagedActions{}, nil
}
if config.Type == "Timeout" {
return NewTimeOut(config, c)
}
if config.Type == "Command" {
return NewCommand(config, c)
}
if config.Type == "Shutdown" {
return NewShutdown(config, c)
func NewSingleAction(config internal.ActionConfig, c ActionResultChan) (Action, error) {
for _, availableAction := range GetAllActions() {
if config.Type == availableAction.GetName() {
return availableAction.Create(config, c)
}
}
return nil, fmt.Errorf("Error parsing config: Action with type %s does not exists", config.Type)
@@ -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)
@@ -111,11 +117,21 @@ func NewAction(config []internal.ActionConfig) (Action, error) {
return stagedActions, nil
}
func GetDocumenters() []internal.Documenter {
return []internal.Documenter{
func GetAllActions() []DocumentedAction {
return []DocumentedAction{
Printer{},
TimeOut{},
Command{},
Shutdown{},
}
}
func GetDocumenters() []internal.Documenter {
var result []internal.Documenter
for _, action := range GetAllActions() {
result = append(result, action)
}
return result
}

View File

@@ -9,20 +9,20 @@ import (
type Printer struct {
Message string
ActionChan chan bool
ActionChan ActionResultChan
}
func (p Printer) Execute() {
fmt.Printf("Print action fires. Message: %s", p.Message)
p.ActionChan <- true
fmt.Printf("Print action fires. Message: %s\n", p.Message)
p.ActionChan <- nil
}
func (p Printer) DryExecute() {
fmt.Printf("Print action fire test. Message: %s", p.Message)
p.ActionChan <- true
fmt.Printf("Print action fire test. Message: %s\n", p.Message)
p.ActionChan <- nil
}
func NewPrint(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 +39,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: "\"\"",
},
}
}

View File

@@ -8,27 +8,27 @@ import (
)
type Shutdown struct {
ActionChan chan bool
ActionChan ActionResultChan
}
func (c Shutdown) DryExecute() {
func (s Shutdown) DryExecute() {
fmt.Println("Test Shutdown executed...")
c.ActionChan <- true
s.ActionChan <- nil
}
func (c Shutdown) Execute() {
func (s Shutdown) Execute() {
if err := exec.Command("shutdown", "-h", "now").Run(); err != nil {
fmt.Println("Failed to initiate shutdown:", err)
}
fmt.Println("Shutdown executed...")
c.ActionChan <- true
s.ActionChan <- nil
}
func NewShutdown(config internal.ActionConfig, c chan bool) (Action, error) {
func (s Shutdown) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return Shutdown{c}, nil
}
@@ -37,7 +37,15 @@ 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",
}
`
}
func (p Shutdown) GetOptions() []internal.ConfigOption {

View File

@@ -10,7 +10,7 @@ import (
type TimeOut struct {
Duration time.Duration
ActionChan chan bool
ActionChan ActionResultChan
}
func (t TimeOut) DryExecute() {
@@ -18,12 +18,12 @@ func (t TimeOut) DryExecute() {
}
func (t TimeOut) Execute() {
fmt.Printf("Waiting %d seconds\n", t.Duration/time.Second)
time.Sleep(t.Duration)
t.ActionChan <- true
fmt.Printf("Waiting %d seconds\n", t.Duration)
time.Sleep(time.Duration(t.Duration) * time.Second)
t.ActionChan <- nil
}
func NewTimeOut(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,11 +40,25 @@ 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 {
return []internal.ConfigOption{
{"duration", "string", "duration in seconds", "0"},
{"duration", "int", "duration in seconds", "0"},
}
}

103
actions/unix_command.go Normal file
View File

@@ -0,0 +1,103 @@
package actions
import (
"encoding/json"
"fmt"
"os/exec"
"strings"
"unknown.com/gokill/internal"
)
type Command struct {
Command string `json:"command"`
ActionChan ActionResultChan
}
func (c Command) DryExecute() {
fmt.Printf("Test Executing Command:\n%s ", c.Command)
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() {
command, args, err := c.splitCommandString()
fmt.Println("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())
}
fmt.Println(string(stdout[:]))
c.ActionChan <- nil
}
func CreateCommand(config internal.ActionConfig, c ActionResultChan) (Command, error) {
result := Command{}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return Command{}, err
}
if result.Command == "" {
return Command{}, internal.OptionMissingError{"command"}
}
result.ActionChan = c
return result, nil
}
func (cc Command) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
return CreateCommand(config, c)
}
func (p Command) GetName() string {
return "Command"
}
func (p Command) GetDescription() string {
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", ""},
}
}

View File

@@ -0,0 +1,102 @@
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 {
fmt.Println(err)
return err
}
defer f.Close()
_, err = f.WriteString(getMarkdown(documenter))
if err != nil {
fmt.Println(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")
flag.Parse()
if *outputPath == "" {
printDocumentersSummary()
return
}
if len(*outputPath) > 1 {
*outputPath = strings.TrimSuffix(*outputPath, "/")
}
writeDocumentersToFiles(*outputPath)
}

View File

@@ -21,7 +21,7 @@ func GetDocumentation() string {
result += fmt.Sprintf("\n### %v\nDescription: %v \nValues:\n", act.GetName(), act.GetDescription())
for _, opt := range act.GetOptions() {
result += fmt.Sprintf("- Name: %v\n\t- Type: %v\n\t- Descr: %v\n\t- Default: %v\n",
result += fmt.Sprintf("- Name: **%v**\n\t- Type: %v\n\t- Descr: %v\n\t- Default: %v\n",
opt.Name, opt.Type, opt.Description, opt.Default)
result += "\n\n"
}

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@

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

@@ -0,0 +1,23 @@
# 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
}
}
```
To get a list of all actions and their options from the commandline run ``` gokill -d ```

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
'';
}

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

@@ -0,0 +1,21 @@
# 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
}
```
To get a list of all triggers and their options from the commandline run ```gokill -d```

27
flake.lock generated Normal file
View File

@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1698553279,
"narHash": "sha256-T/9P8yBSLcqo/v+FTOBK+0rjzjPMctVymZydbvR/Fak=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "90e85bc7c1a6fc0760a94ace129d3a1c61c3d035",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

164
flake.nix Normal file
View File

@@ -0,0 +1,164 @@
{
description = "A very basic flake";
#nixpkgs for testing framework
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" ];
pkgs = nixpkgs.legacyPackages."x86_64-linux";
in
{
devShell."x86_64-linux" = pkgs.mkShell {
packages = with pkgs; [
go
gotools
mdbook
];
};
packages.x86_64-linux.gokill = nixpkgs.legacyPackages.x86_64-linux.buildGoModule rec {
pname = "gokill";
version = "1.0";
vendorHash = "sha256-aKEOMeW9QVSLsSuHV4b1khqM0rRrMjJ6Eu5RjY+6V8k=";
src = ./.;
postInstall = ''
'';
};
packages.x86_64-linux.gokill-docbuilder = nixpkgs.legacyPackages.x86_64-linux.buildGoModule rec {
pname = "docbuilder";
version = "1.0";
vendorHash = null;
buildFLags = "-o . $dest/cmd/gokill/docbuilder";
src = ./.;
postInstall = ''
'';
};
packages.x86_64-linux.docs = pkgs.callPackage (import ./docs/default.nix) { self = self; };
packages.x86_64-linux.default = self.packages.x86_64-linux.gokill;
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
'';
};
extraConfig = lib.mkOption {
type = lib.types.str;
description = lib.mdDoc ''
gokill config.json
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.gokill = {
description = "gokill daemon";
serviceConfig = {
Type = "simple";
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
Restart = "on-failure";
};
wantedBy = [ "default.target" ];
};
};
};
packages.x86_64-linux.testVm =
let
nixos = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
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
}
]
}
]
'';
users.users.root.password = "root";
virtualisation.vmVariant.virtualisation.graphics = false;
}
];
};
in
nixos.config.system.build.vm;
apps.x86_64-linux.testVm = {
type = "app";
program = builtins.toString (nixpkgs.legacyPackages."x86_64-linux".writeScript "vm" ''
${self.packages."x86_64-linux".testVm}/bin/run-nixos-vm
'');
};
apps.x86_64-linux.docs = {
type = "app";
program = builtins.toString (nixpkgs.legacyPackages."x86_64-linux".writeScript "docs" ''
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."x86_64-linux".docs}/share/doc'');
};
};
}

2
go.mod
View File

@@ -1,3 +1,3 @@
module unknown.com/gokill
go 1.20
go 1.21.3

View File

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

View File

@@ -49,8 +49,7 @@ func (t EthernetDisconnect) Listen() {
actions.Fire(t.action)
}
// func NewTimeOut(d time.Duration, action actions.Action) EthernetDisconnect {
func NewEthernetDisconnect(config internal.KillSwitchConfig) (EthernetDisconnect, error) {
func CreateEthernetDisconnect(config internal.KillSwitchConfig) (EthernetDisconnect, error) {
result := EthernetDisconnect{
WaitTillConnected: true,
}
@@ -76,6 +75,10 @@ func NewEthernetDisconnect(config internal.KillSwitchConfig) (EthernetDisconnect
return result, nil
}
func (e EthernetDisconnect) Create(config internal.KillSwitchConfig) (Trigger, error) {
return CreateEthernetDisconnect(config)
}
func (p EthernetDisconnect) GetName() string {
return "EthernetDisconnect"
}
@@ -84,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"},

82
triggers/ethernet_test.go Normal file
View File

@@ -0,0 +1,82 @@
package triggers
import (
"testing"
"unknown.com/gokill/internal"
)
func TestEthernetDisconnetConfig(t *testing.T) {
type EthernetTest struct {
testConfig internal.KillSwitchConfig
expectedError error
expectedResult EthernetDisconnect
}
testConfigs := []EthernetTest{
EthernetTest{
testConfig: internal.KillSwitchConfig{
Options: []byte("{}"),
},
expectedError: internal.OptionMissingError{"interfaceName"},
expectedResult: EthernetDisconnect{},
},
EthernetTest{
testConfig: internal.KillSwitchConfig{
Options: []byte(`{
"waitTillConnected": false
}`),
},
expectedError: internal.OptionMissingError{"interfaceName"},
expectedResult: EthernetDisconnect{},
},
EthernetTest{
testConfig: internal.KillSwitchConfig{
Options: []byte(`{
"interfaceName": "eth0",
"waitTillConnected": false
}`),
},
expectedError: nil,
expectedResult: EthernetDisconnect{WaitTillConnected: false, InterfaceName: "eth0"},
},
EthernetTest{
testConfig: internal.KillSwitchConfig{
Options: []byte(`{
"interfaceName": "eth0",
"waitTillConnected": true
}`),
},
expectedError: nil,
expectedResult: EthernetDisconnect{WaitTillConnected: true, InterfaceName: "eth0"},
},
EthernetTest{
testConfig: internal.KillSwitchConfig{
Options: []byte("{ \"interfaceName\": \"eth0\" }"),
},
expectedError: nil,
expectedResult: EthernetDisconnect{WaitTillConnected: true, InterfaceName: "eth0"},
},
}
for _, testConfig := range testConfigs {
result, err := CreateEthernetDisconnect(testConfig.testConfig)
if err != testConfig.expectedError {
t.Errorf("Error was incorrect, got: %s, want: %s.", err, testConfig.expectedError)
}
if result.WaitTillConnected != testConfig.expectedResult.WaitTillConnected {
t.Errorf("WaitTillConnected was incorrect, got: %v, want: %v.", result, testConfig.expectedResult)
}
if result.InterfaceName != testConfig.expectedResult.InterfaceName {
t.Errorf("InterfaceName was incorrect, got: %v, want: %v.", result, testConfig.expectedResult)
}
}
}

View File

@@ -1,5 +1,4 @@
package triggers
import (
"encoding/json"
"fmt"
@@ -22,7 +21,7 @@ func (t TimeOut) Listen() {
actions.Fire(t.action)
}
func NewTimeOut(config internal.KillSwitchConfig) (TimeOut, error) {
func (t TimeOut) Create(config internal.KillSwitchConfig) (Trigger, error) {
var result TimeOut
err := json.Unmarshal(config.Options, &result)
@@ -45,11 +44,25 @@ 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 {
return []internal.ConfigOption{
{"duration", "string", "duration in seconds", "0"},
{"duration", "int", "duration in seconds", "0"},
}
}

View File

@@ -8,23 +8,38 @@ import (
type Trigger interface {
Listen()
Create(internal.KillSwitchConfig) (Trigger, error)
}
type DocumentedTrigger interface {
internal.Documenter
Trigger
}
func NewTrigger(config internal.KillSwitchConfig) (Trigger, error) {
if config.Type == "Timeout" {
return NewTimeOut(config)
}
if config.Type == "EthernetDisconnect" {
return NewEthernetDisconnect(config)
for _, availableTrigger := range GetAllTriggers() {
if config.Type == availableTrigger.GetName() {
return availableTrigger.Create(config)
}
}
return nil, fmt.Errorf("Error parsing config: Trigger with type %s does not exists", config.Type)
}
func GetDocumenters() []internal.Documenter {
return []internal.Documenter{
func GetAllTriggers() []DocumentedTrigger {
return []DocumentedTrigger{
TimeOut{},
EthernetDisconnect{},
UsbDisconnect{},
}
}
func GetDocumenters() []internal.Documenter {
var result []internal.Documenter
for _, action := range GetAllTriggers() {
result = append(result, action)
}
return result
}

112
triggers/usb.go Normal file
View File

@@ -0,0 +1,112 @@
package triggers
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
"unknown.com/gokill/actions"
"unknown.com/gokill/internal"
)
type UsbDisconnect struct {
WaitTillConnected bool `json:"waitTillConnected"`
DeviceName string `json:"deviceName"`
action actions.Action
}
func isUsbConnected(deviceName string) bool {
devicePath := "/dev/disk/by-id/" + deviceName
_, err := os.Open(devicePath)
if errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func (t UsbDisconnect) Listen() {
if t.WaitTillConnected {
for !isUsbConnected(t.DeviceName) {
time.Sleep(1 * time.Second)
}
fmt.Printf("Device %s detected.\n", t.DeviceName)
fmt.Println("UsbDisconnect Trigger is Armed")
}
for {
if !isUsbConnected(t.DeviceName) {
break
}
time.Sleep(1 * time.Second)
}
actions.Fire(t.action)
}
func CreateUsbDisconnect(config internal.KillSwitchConfig) (UsbDisconnect, error) {
result := UsbDisconnect{
WaitTillConnected: true,
}
err := json.Unmarshal(config.Options, &result)
if err != nil {
return UsbDisconnect{}, err
}
if result.DeviceName == "" {
return UsbDisconnect{}, internal.OptionMissingError{"deviceName"}
}
action, err := actions.NewAction(config.Actions)
if err != nil {
return UsbDisconnect{}, err
}
result.action = action
return result, nil
}
func (e UsbDisconnect) Create(config internal.KillSwitchConfig) (Trigger, error) {
return CreateUsbDisconnect(config)
}
func (p UsbDisconnect) GetName() string {
return "UsbDisconnect"
}
func (p UsbDisconnect) GetDescription() string {
return "Triggers when given usb drive is disconnected"
}
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"},
{"deviceId", "string", "Name of device under /dev/disk/by-id/", "\"\""},
}
}