Initial prototype
Change-Id: I60a94e90aab48dfcf7c1f03fe5613d1db7d0df95
diff --git a/cmd/hichipbridge/.gitignore b/cmd/hichipbridge/.gitignore
new file mode 100644
index 0000000..3adf02a
--- /dev/null
+++ b/cmd/hichipbridge/.gitignore
@@ -0,0 +1,2 @@
+hichipbridge
+config.json
diff --git a/cmd/hichipbridge/Dockerfile b/cmd/hichipbridge/Dockerfile
new file mode 100644
index 0000000..f303928
--- /dev/null
+++ b/cmd/hichipbridge/Dockerfile
@@ -0,0 +1,35 @@
+FROM golang:latest as builder
+
+ENV CGO_ENABLED=0
+
+ENV GO111MODULE=on
+ENV GOPROXY=https://proxy.golang.org
+
+RUN mkdir /gocache
+ENV GOCACHE /gocache
+
+COPY go.mod /go/src/gomodules.avm99963.com/hichip2mqtt/go.mod
+COPY go.sum /go/src/gomodules.avm99963.com/hichip2mqtt/go.sum
+
+WORKDIR /go/src/gomodules.avm99963.com/hichip2mqtt
+
+# Optimization for iterative docker build speed, not necessary for correctness:
+RUN go install github.com/eclipse/paho.mqtt.golang
+RUN go install github.com/flashmob/go-guerrilla
+RUN go install github.com/go-sql-driver/mysql
+RUN go install github.com/sirupsen/logrus
+RUN go install github.com/spf13/cobra
+RUN go install gomodules.avm99963.com/forks/parsemail
+
+WORKDIR /go/src/gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+COPY . /go/src/gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+RUN go install gomodules.avm99963.com/hichip2mqtt/cmd/hichipbridge
+
+FROM alpine
+LABEL maintainer "me@avm99963.com"
+
+RUN apk add --no-cache tini
+COPY --from=builder /go/bin/hichipbridge /
+ENTRYPOINT ["/sbin/tini", "--", "/hichipbridge", "serve"]
diff --git a/cmd/hichipbridge/Makefile b/cmd/hichipbridge/Makefile
new file mode 100644
index 0000000..f3c5687
--- /dev/null
+++ b/cmd/hichipbridge/Makefile
@@ -0,0 +1,11 @@
+MUTABLE_VERSION ?= latest
+VERSION ?= $(shell git rev-parse --short HEAD)
+
+IMAGE_PROD := hichipbridge
+
+docker-prod: Dockerfile
+ docker build --force-rm -f Dockerfile --tag=$(IMAGE_PROD):$(VERSION) ../../
+ docker tag $(IMAGE_PROD):$(VERSION) $(IMAGE_PROD):$(MUTABLE_VERSION)
+
+push-prod: docker-prod
+ docker push $(IMAGE_PROD):$(VERSION)
diff --git a/cmd/hichipbridge/config.sample.json b/cmd/hichipbridge/config.sample.json
new file mode 100644
index 0000000..a0ad7d7
--- /dev/null
+++ b/cmd/hichipbridge/config.sample.json
@@ -0,0 +1,14 @@
+{
+ "log_file": "stderr",
+ "log_level": "debug",
+ "backend_config": {
+ "log_received_mails": false
+ },
+ "servers": [{
+ "is_enabled": true,
+ "max_size": 1000000,
+ "listen_interface": "0.0.0.0:25",
+ "timeout": 30,
+ "max_clients": 3
+ }]
+}
diff --git a/cmd/hichipbridge/docker-compose.yml b/cmd/hichipbridge/docker-compose.yml
new file mode 100644
index 0000000..92e8913
--- /dev/null
+++ b/cmd/hichipbridge/docker-compose.yml
@@ -0,0 +1,10 @@
+version: "3.9"
+services:
+ bridge:
+ image: "hichipbridge:latest"
+ ports:
+ - "8881:25"
+ env_file: .env
+ volumes:
+ - ./config.json:/config.json:ro
+ restart: unless-stopped
diff --git a/cmd/hichipbridge/env.sample b/cmd/hichipbridge/env.sample
new file mode 100644
index 0000000..4e0f25b
--- /dev/null
+++ b/cmd/hichipbridge/env.sample
@@ -0,0 +1,14 @@
+# Host for the SMTP server where you're going to receive email notifications
+HICHIPBRIDGE_SMTP_HOST="smtp-server.example.com"
+# MQTT broker endpoint
+HICHIPBRIDGE_MQTT_BROKER="mqtt://mqtt-broker.example.com:1883"
+# MQTT client id
+HICHIPBRIDGE_MQTT_CLIENTID="hichipbridge"
+# MQTT username
+HICHIPBRIDGE_MQTT_USERNAME="user"
+# MQTT password
+HICHIPBRIDGE_MQTT_PASSWORD="password"
+# Prefix for the MQTT topics where events will be saved
+HICHIPBRIDGE_MQTT_TOPIC="hichipbridge"
+# Token for the email notifications
+HICHIPBRIDGE_EMAIL_TOKEN="superconfidentialtoken;)"
diff --git a/cmd/hichipbridge/main.go b/cmd/hichipbridge/main.go
new file mode 100644
index 0000000..aa12bc4
--- /dev/null
+++ b/cmd/hichipbridge/main.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func main() {
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Println(err)
+ os.Exit(-1)
+ }
+}
diff --git a/cmd/hichipbridge/root.go b/cmd/hichipbridge/root.go
new file mode 100644
index 0000000..42eebdb
--- /dev/null
+++ b/cmd/hichipbridge/root.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "hichipbridge",
+ Short: "hichip2mqtt bridge daemon",
+ Long: `Daemon which serves as a bridge for hichip IP cameras and MQTT.
+Basically, it converts motion trigger emails notifications received into a state
+published in a MQTT topic.`,
+ Run: nil,
+}
+
+var (
+ verbose bool
+)
+
+func init() {
+ cobra.OnInitialize()
+ rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
+ "print out more debug information")
+ rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
+ if verbose {
+ logrus.SetLevel(logrus.DebugLevel)
+ } else {
+ logrus.SetLevel(logrus.InfoLevel)
+ }
+ }
+}
diff --git a/cmd/hichipbridge/serve.go b/cmd/hichipbridge/serve.go
new file mode 100644
index 0000000..b16de0a
--- /dev/null
+++ b/cmd/hichipbridge/serve.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/flashmob/go-guerrilla"
+ "github.com/flashmob/go-guerrilla/backends"
+ "github.com/flashmob/go-guerrilla/log"
+
+ // Choose iconv or mail/encoding package which uses golang.org/x/net/html/charset
+ //_ "github.com/flashmob/go-guerrilla/mail/iconv"
+ _ "github.com/flashmob/go-guerrilla/mail/encoding"
+
+ "github.com/spf13/cobra"
+
+ _ "github.com/go-sql-driver/mysql"
+
+ "gomodules.avm99963.com/hichip2mqtt/mqtt_processor"
+)
+
+const (
+ defaultPidFile = "/var/run/hichipbridge.pid"
+)
+
+var (
+ configPath string
+ pidFile string
+
+ serveCmd = &cobra.Command{
+ Use: "serve",
+ Short: "start the daemon and start all available SMTP servers",
+ Run: serve,
+ }
+
+ signalChannel = make(chan os.Signal, 1) // for trapping SIGHUP and friends
+ mainlog log.Logger
+
+ d guerrilla.Daemon
+)
+
+func init() {
+ // log to stderr on startup
+ var err error
+ mainlog, err = log.GetLogger(log.OutputStderr.String(), log.InfoLevel.String())
+ if err != nil && mainlog != nil {
+ mainlog.WithError(err).Errorf("Failed creating a logger to %s", log.OutputStderr)
+ }
+ cfgFile := "config.json"
+ serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
+ cfgFile, "Path to the configuration file")
+ // intentionally didn't specify default pidFile; value from config is used if flag is empty
+ serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
+ "", "Path to the pid file")
+ rootCmd.AddCommand(serveCmd)
+}
+
+func sigHandler() {
+ signal.Notify(signalChannel,
+ syscall.SIGHUP,
+ syscall.SIGTERM,
+ syscall.SIGQUIT,
+ syscall.SIGINT,
+ syscall.SIGKILL,
+ syscall.SIGUSR1,
+ os.Kill,
+ )
+ for sig := range signalChannel {
+ if sig == syscall.SIGHUP {
+ if ac, err := readConfig(configPath, pidFile); err == nil {
+ _ = d.ReloadConfig(*ac)
+ } else {
+ mainlog.WithError(err).Error("Could not reload config")
+ }
+ } else if sig == syscall.SIGUSR1 {
+ if err := d.ReopenLogs(); err != nil {
+ mainlog.WithError(err).Error("reopening logs failed")
+ }
+ } else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT || sig == os.Kill {
+ mainlog.Infof("Shutdown signal caught")
+ go func() {
+ select {
+ // exit if graceful shutdown not finished in 60 sec.
+ case <-time.After(time.Second * 60):
+ mainlog.Error("graceful shutdown timed out")
+ os.Exit(1)
+ }
+ }()
+ d.Shutdown()
+ mainlog.Infof("Shutdown completed, exiting.")
+ return
+ } else {
+ mainlog.Infof("Shutdown, unknown signal caught")
+ return
+ }
+ }
+}
+
+func serve(cmd *cobra.Command, args []string) {
+ logVersion()
+ d = guerrilla.Daemon{Logger: mainlog}
+ d.AddProcessor("MQTT", mqtt_processor.Processor)
+
+ c, err := readConfig(configPath, pidFile)
+ if err != nil {
+ mainlog.WithError(err).Fatal("Error while reading config")
+ }
+ _ = d.SetConfig(*c)
+
+ // Check that max clients is not greater than system open file limit.
+ if ok, maxClients, fileLimit := guerrilla.CheckFileLimit(c); !ok {
+ mainlog.Fatalf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
+ "Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
+ }
+
+ err = d.Start()
+ if err != nil {
+ mainlog.WithError(err).Error("Error(s) when creating new server(s)")
+ os.Exit(1)
+ }
+ sigHandler()
+
+}
+
+func lookupPrefixedEnv(coreName string) (string, error) {
+ name := "HICHIPBRIDGE_" + coreName
+ v, ok := os.LookupEnv(name)
+ if !ok {
+ return v, fmt.Errorf("Environment variable %s isn't set", name)
+ }
+
+ return v, nil
+}
+
+// ReadConfig is called at startup, or when a SIG_HUP is caught
+func readConfig(path string, pidFile string) (*guerrilla.AppConfig, error) {
+ // Environment variables where some settings are configured (without the prefix "HICHIPBRIDGE_"):
+ envNames := []string{
+ "SMTP_HOST",
+ "MQTT_BROKER",
+ "MQTT_CLIENTID",
+ "MQTT_USERNAME",
+ "MQTT_PASSWORD",
+ "MQTT_TOPIC",
+ "EMAIL_TOKEN",
+ }
+
+ // Set default configuration (overridable)
+ defaultAppConfig := guerrilla.AppConfig{
+ BackendConfig: backends.BackendConfig{
+ "save_workers_size": 1,
+ "log_received_mails": false,
+ },
+ }
+
+ d.SetConfig(defaultAppConfig)
+
+ // Load in the config.
+ appConfig, err := d.LoadConfig(path)
+ if err != nil {
+ return &appConfig, fmt.Errorf("could not read config file: %s", err.Error())
+ }
+
+ // override config pidFile with with flag from the command line
+ if len(pidFile) > 0 {
+ appConfig.PidFile = pidFile
+ } else if len(appConfig.PidFile) == 0 {
+ appConfig.PidFile = defaultPidFile
+ }
+ if verbose {
+ appConfig.LogLevel = "debug"
+ }
+
+ // Override compulsory settings
+ var envs = make(map[string]string)
+ for _, env := range envNames {
+ v, err := lookupPrefixedEnv(env)
+ if err != nil {
+ return &appConfig, err
+ }
+ envs[env] = v
+ }
+
+ appConfig.AllowedHosts = []string{envs["SMTP_HOST"]}
+ appConfig.BackendConfig = backends.BackendConfig{
+ "save_process": "HeadersParser|Hasher|Debugger|MQTT",
+ "validate_process": "",
+ "primary_mail_host": envs["SMTP_HOST"],
+ "mqtt_broker": envs["MQTT_BROKER"],
+ "mqtt_clientid": envs["MQTT_CLIENTID"],
+ "mqtt_username": envs["MQTT_USERNAME"],
+ "mqtt_password": envs["MQTT_PASSWORD"],
+ "mqtt_topic": envs["MQTT_TOPIC"],
+ "mqtt_email_token": envs["EMAIL_TOKEN"],
+ }
+
+ for i, _ := range appConfig.Servers {
+ appConfig.Servers[i].Hostname = envs["SMTP_HOST"]
+ }
+
+ return &appConfig, nil
+}
diff --git a/cmd/hichipbridge/version.go b/cmd/hichipbridge/version.go
new file mode 100644
index 0000000..269a5fe
--- /dev/null
+++ b/cmd/hichipbridge/version.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "github.com/spf13/cobra"
+
+ "github.com/flashmob/go-guerrilla"
+)
+
+var versionCmd = &cobra.Command{
+ Use: "version",
+ Short: "Print the version info",
+ Long: `Every software has a version. This is Guerrilla's`,
+ Run: func(cmd *cobra.Command, args []string) {
+ logVersion()
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(versionCmd)
+}
+
+func logVersion() {
+ mainlog.Infof("Using guerrilla %s", guerrilla.Version)
+ mainlog.Debugf("Build Time: %s", guerrilla.BuildTime)
+ mainlog.Debugf("Commit: %s", guerrilla.Commit)
+}