jlelse
/
kis3
Archived
1
Fork 0

Add Daily Email reports feature (#4)

This commit is contained in:
Jan-Lukas Else 2019-05-01 11:46:52 +02:00 committed by GitHub
parent 903dab597f
commit 85f16f1d0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 210 additions and 181 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea/ .idea/
data/ data/
config.json

View File

@ -6,27 +6,41 @@
KISSS is really easy to install via Docker. KISSS is really easy to install via Docker.
docker run -d --name kis3 -e ENVVAR=VARVALUE -e ... -p 8080:8080 -v kis3:/app/data kis3/kis3 docker run -d --name kis3 -p 8080:8080 -v kis3:/app/data -v ${pwd}/config.json:/app/config.json kis3/kis3
Replace ENVVAR and VARVALUE with the environment variables from the configuration.
Depending on your setup, replace `-p 8080:8080` with your custom port configuration. KISSS listens to port 8080 by default, but you can also change this via the configuration. Depending on your setup, replace `-p 8080:8080` with your custom port configuration. KISSS listens to port 8080 by default, but you can also change this via the configuration.
To persist the data KISSS collects, you should mount a volume or a folder to `/app/data`. When mounting an folder, give writing permissions to UID 100, because it is a non-root image to make it more secure. To persist the data KISSS collects, you should mount a volume or a folder to `/app/data`. When mounting an folder, give writing permissions to UID 100, because it is a non-root image to make it more secure.
You should also mount a configuration file to `/app/config.json`.
It's also possible to use KISSS without Docker, but for that you need to compile it yourself. In the future there will be executables without dependencies available. It's also possible to use KISSS without Docker, but for that you need to compile it yourself. In the future there will be executables without dependencies available.
## Configuration ## Configuration
You can configure some settings using environment variables: You can configure some settings using a `config.json` file in the working directory or provide a custom path to it using the `-c` CLI flag:
`PORT` (`8080`): Set the port to which KISSS should listen `port` (`8080`): Set the port to which KISSS should listen
`DNT` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send `dnt` (`true`): Set whether or not KISSS should respect Do-Not-Track headers some browsers send
`DB_PATH` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`). `dbPath` (`data/kis3.db`): Set the path for the SQLite database (relative to the working directory - in the Docker container it's `/app`).
You can make the statistics private and only accessible with authentication by setting both `STATS_USERNAME` and `STATS_PASSWORD` to a username and password. If only one or none is set, the statistics are accessible without authorization and public to anyone. You can make the statistics private and only accessible with authentication by setting both `statsUsername` and `statsPassword` to a username and password. If only one or none is set, the statistics are accessible without authorization and public to anyone.
The configuration file can look like this:
```json
{
"port": 8080,
"dnt": true,
"dbPath": "data/kis3.db",
"statsUsername": "myusername",
"statsPassword": "mysecretpassword"
}
```
If you specify an environment variable (`PORT`, `DNT`, `DB_PATH`, `STATS_USERNAME`, `STATS_PASSWORD`), that will override the settings from the configuration file.
## Add to website ## Add to website
@ -56,6 +70,31 @@ The following filters are available:
`format`: the format to represent the data, default is `plain` for a simple plain text list, `json` for a JSON response or `chart` for a chart generated with ChartJS in the browser `format`: the format to represent the data, default is `plain` for a simple plain text list, `json` for a JSON response or `chart` for a chart generated with ChartJS in the browser
## Daily email reports
KISSS has a feature that can send you daily email reports. It basically requests the statistics and sends the response via email. You can configure it by adding report configurations to the configuration file:
```json
{
// Other configurations...
"reports": [
{
"name": "Daily stats from KISSS",
"time": "15:00",
"query": "view=pages&orderrow=second&order=desc",
"from": "myemailaddress@mydomain.tld",
"to": "myemailaddress@mydomain.tld",
"smtpHost": "mail.mydomain.tld:587",
"smtpUser": "myemailaddress@mydomain.tld",
"smtpPassword": "mysecretpassword"
},
{
// Additional reports...
}
]
}
```
## License ## License
KISSS is licensed under the MIT license, so you can do basically everything with it, but nevertheless, please contribute your improvements to make KISSS better for everyone. See the LICENSE.txt file. KISSS is licensed under the MIT license, so you can do basically everything with it, but nevertheless, please contribute your improvements to make KISSS better for everyone. See the LICENSE.txt file.

105
config.go
View File

@ -1,68 +1,87 @@
package main package main
import ( import (
"encoding/json"
"flag"
"io/ioutil"
"os" "os"
"strconv" "strconv"
) )
type config struct { type config struct {
port string Port string `json:"port"`
dnt bool Dnt bool `json:"dnt"`
dbPath string DbPath string `json:"dbPath"`
statsAuth bool StatsUsername string `json:"statsUsername"`
statsUsername string StatsPassword string `json:"statsPassword"`
statsPassword string Reports []report `json:"reports"`
}
type report struct {
Name string `json:"name"`
Time string `json:"time"`
To string `json:"to"`
SmtpUser string `json:"smtpUser"`
SmtpPassword string `json:"smtpPassword"`
SmtpHost string `json:"smtpHost"`
From string `json:"from"`
Query string `json:"query"`
} }
var ( var (
appConfig = &config{} appConfig = &config{
Port: "8080",
Dnt: true,
DbPath: "data/kis3.db",
StatsUsername: "",
StatsPassword: "",
}
) )
func init() { func init() {
appConfig.port = port() parseConfigFile(appConfig)
appConfig.dnt = dnt() // Replace values that are set via environment vars (to make it compatible with old method)
appConfig.dbPath = dbPath() overwriteEnvVarValues(appConfig)
appConfig.statsUsername = statsUsername()
appConfig.statsPassword = statsPassword()
appConfig.statsAuth = statsAuth(appConfig)
} }
func port() string { func parseConfigFile(appConfig *config) {
port := os.Getenv("PORT") configFile := flag.String("c", "config.json", "Config file")
if len(port) != 0 { flag.Parse()
return port configJson, e := ioutil.ReadFile(*configFile)
} else {
return "8080"
}
}
func dnt() bool {
dnt := os.Getenv("DNT")
dntBool, e := strconv.ParseBool(dnt)
if e != nil { if e != nil {
dntBool = true return
} }
return dntBool e = json.Unmarshal([]byte(configJson), appConfig)
} if e != nil {
return
func dbPath() (dbPath string) {
dbPath = os.Getenv("DB_PATH")
if len(dbPath) == 0 {
dbPath = "data/kis3.db"
} }
return return
} }
func statsUsername() (username string) { func overwriteEnvVarValues(appConfig *config) {
username = os.Getenv("STATS_USERNAME") port, set := os.LookupEnv("PORT")
return if set {
appConfig.Port = port
}
dntString, set := os.LookupEnv("DNT")
dntBool, e := strconv.ParseBool(dntString)
if set && e == nil {
appConfig.Dnt = dntBool
}
dbPath, set := os.LookupEnv("DB_PATH")
if set {
appConfig.DbPath = dbPath
}
username, set := os.LookupEnv("STATS_USERNAME")
if set {
appConfig.StatsUsername = username
}
password, set := os.LookupEnv("STATS_PASSWORD")
if set {
appConfig.StatsPassword = password
}
} }
func statsPassword() (password string) { func (ac *config) statsAuth() bool {
password = os.Getenv("STATS_PASSWORD") return len(ac.StatsUsername) > 0 && len(ac.StatsPassword) > 0
return
}
func statsAuth(ac *config) bool {
return len(ac.statsUsername) > 0 && len(ac.statsPassword) > 0
} }

View File

@ -1,131 +1,32 @@
package main package main
import ( import (
"os"
"testing" "testing"
) )
func Test_port(t *testing.T) { func Test_config_statsAuth(t *testing.T) {
type fields struct {
StatsUsername string
StatsPassword string
}
tests := []struct { tests := []struct {
name string name string
envVar string fields fields
want string
}{
{name: "default", envVar: "", want: "8080"},
{name: "custom", envVar: "1234", want: "1234"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("PORT", tt.envVar)
if got := port(); got != tt.want {
t.Errorf("port() = %v, want %v", got, tt.want)
}
})
}
}
func Test_dnt(t *testing.T) {
tests := []struct {
name string
envVar string
want bool want bool
}{ }{
{name: "default", envVar: "", want: true}, {"No username nor password", fields{"", ""}, false},
{envVar: "true", want: true}, {"Only username", fields{"abc", ""}, false},
{envVar: "t", want: true}, {"Only password", fields{"", "abc"}, false},
{envVar: "TRUE", want: true}, {"Username and password", fields{"abc", "abc"}, true},
{envVar: "1", want: true},
{envVar: "false", want: false},
{envVar: "f", want: false},
{envVar: "0", want: false},
{envVar: "abc", want: true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("DNT", tt.envVar) ac := &config{
if got := dnt(); got != tt.want { StatsUsername: tt.fields.StatsUsername,
t.Errorf("dnt() = %v, want %v", got, tt.want) StatsPassword: tt.fields.StatsPassword,
} }
}) if got := ac.statsAuth(); got != tt.want {
} t.Errorf("config.statsAuth() = %v, want %v", got, tt.want)
}
func Test_dbPath(t *testing.T) {
tests := []struct {
name string
envVar string
wantDbPath string
}{
{name: "default", envVar: "", wantDbPath: "data/kis3.db"},
{envVar: "kis3.db", wantDbPath: "kis3.db"},
{envVar: "data.db", wantDbPath: "data.db"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("DB_PATH", tt.envVar)
if gotDbPath := dbPath(); gotDbPath != tt.wantDbPath {
t.Errorf("dbPath() = %v, want %v", gotDbPath, tt.wantDbPath)
}
})
}
}
func Test_statsUsername(t *testing.T) {
tests := []struct {
name string
envVar string
wantUsername string
}{
{name: "default", envVar: "", wantUsername: ""},
{envVar: "abc", wantUsername: "abc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("STATS_USERNAME", tt.envVar)
if gotUsername := statsUsername(); gotUsername != tt.wantUsername {
t.Errorf("statsUsername() = %v, want %v", gotUsername, tt.wantUsername)
}
})
}
}
func Test_statsPassword(t *testing.T) {
tests := []struct {
name string
envVar string
wantPassword string
}{
{name: "default", envVar: "", wantPassword: ""},
{envVar: "def", wantPassword: "def"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_ = os.Setenv("STATS_PASSWORD", tt.envVar)
if gotPassword := statsPassword(); gotPassword != tt.wantPassword {
t.Errorf("statsPassword() = %v, want %v", gotPassword, tt.wantPassword)
}
})
}
}
func Test_statsAuth(t *testing.T) {
type args struct {
ac *config
}
tests := []struct {
name string
args args
want bool
}{
{name: "default", args: struct{ ac *config }{ac: &config{}}, want: false},
{name: "only username set", args: struct{ ac *config }{ac: &config{statsUsername: "abc"}}, want: false},
{name: "only password set", args: struct{ ac *config }{ac: &config{statsPassword: "def"}}, want: false},
{name: "username and password set", args: struct{ ac *config }{ac: &config{statsUsername: "abc", statsPassword: "def"}}, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := statsAuth(tt.args.ac); got != tt.want {
t.Errorf("statsAuth() = %v, want %v", got, tt.want)
} }
}) })
} }

View File

@ -19,10 +19,10 @@ type Database struct {
func initDatabase() (database *Database, e error) { func initDatabase() (database *Database, e error) {
database = &Database{} database = &Database{}
if _, err := os.Stat(appConfig.dbPath); os.IsNotExist(err) { if _, err := os.Stat(appConfig.DbPath); os.IsNotExist(err) {
_ = os.MkdirAll(filepath.Dir(appConfig.dbPath), os.ModePerm) _ = os.MkdirAll(filepath.Dir(appConfig.DbPath), os.ModePerm)
} }
database.sqlDB, e = sql.Open("sqlite3", appConfig.dbPath) database.sqlDB, e = sql.Open("sqlite3", appConfig.DbPath)
if e != nil { if e != nil {
return return
} }

3
go.mod
View File

@ -6,12 +6,15 @@ require (
github.com/go-sql-driver/mysql v1.4.1 // indirect github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/gobuffalo/packr v1.25.0 // indirect github.com/gobuffalo/packr v1.25.0 // indirect
github.com/gobuffalo/packr/v2 v2.2.0 github.com/gobuffalo/packr/v2 v2.2.0
github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/handlers v1.4.0 github.com/gorilla/handlers v1.4.0
github.com/gorilla/mux v1.7.1 github.com/gorilla/mux v1.7.1
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8
github.com/lib/pq v1.1.0 // indirect github.com/lib/pq v1.1.0 // indirect
github.com/mattn/go-sqlite3 v0.0.0-20190424093727-5994cc52dfa8 github.com/mattn/go-sqlite3 v0.0.0-20190424093727-5994cc52dfa8
github.com/mssola/user_agent v0.5.0 github.com/mssola/user_agent v0.5.0
github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051 github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec
github.com/ziutek/mymysql v1.5.4 // indirect github.com/ziutek/mymysql v1.5.4 // indirect
google.golang.org/appengine v1.5.0 // indirect google.golang.org/appengine v1.5.0 // indirect
gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/gorp.v1 v1.7.2 // indirect

6
go.sum
View File

@ -24,10 +24,14 @@ github.com/gobuffalo/packr/v2 v2.2.0 h1:Ir9W9XIm9j7bhhkKE9cokvtTl1vBm62A/fene/ZC
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8 h1:XMe1IsRiRx3E3M50BhP7327VYF4A9RpCFfhHUFW+IeE=
github.com/jordan-wright/email v0.0.0-20190218024454-3ea4d25e7cf8/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -55,6 +59,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec h1:4mCJZnO75zjolpdsj/ToKe7X1oLWm+JJHwS1ez8BkXY=
github.com/whiteshtef/clockwork v0.0.0-20190417075149-ecf7d9abe8ec/go.mod h1:6o8H8sci2q3QxZ4p/U88ggqZuhY3mg34+WE5BuazLsU=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

26
main.go
View File

@ -21,7 +21,9 @@ type kis3 struct {
} }
var ( var (
app = &kis3{} app = &kis3{
staticBox: packr.New("staticFiles", "./static"),
}
) )
func init() { func init() {
@ -30,7 +32,7 @@ func init() {
log.Fatal("Database setup failed:", e) log.Fatal("Database setup failed:", e)
} }
setupRouter() setupRouter()
app.staticBox = packr.New("staticFiles", "./static") setupReports()
} }
func main() { func main() {
@ -49,29 +51,29 @@ func setupRouter() {
viewRouter := app.router.PathPrefix("/view").Subrouter() viewRouter := app.router.PathPrefix("/view").Subrouter()
viewRouter.Use(corsHandler) viewRouter.Use(corsHandler)
viewRouter.Path("").HandlerFunc(trackView) viewRouter.Path("").HandlerFunc(TrackingHandler)
app.router.HandleFunc("/stats", requestStats) app.router.HandleFunc("/stats", StatsHandler)
staticRouter := app.router.PathPrefix("").Subrouter() staticRouter := app.router.PathPrefix("").Subrouter()
staticRouter.Use(corsHandler) staticRouter.Use(corsHandler)
staticRouter.HandleFunc("/kis3.js", serveTrackingScript) staticRouter.HandleFunc("/kis3.js", TrackingScriptHandler)
staticRouter.PathPrefix("").Handler(http.HandlerFunc(HelloResponseHandler)) staticRouter.PathPrefix("").Handler(http.HandlerFunc(HelloResponseHandler))
} }
func startListening() { func startListening() {
port := appConfig.port port := appConfig.Port
addr := ":" + port addr := ":" + port
fmt.Printf("Listening to %s\n", addr) fmt.Printf("Listening to %s\n", addr)
log.Fatal(http.ListenAndServe(addr, app.router)) log.Fatal(http.ListenAndServe(addr, app.router))
} }
func trackView(w http.ResponseWriter, r *http.Request) { func TrackingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0") w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
url := r.URL.Query().Get("url") url := r.URL.Query().Get("url")
ref := r.URL.Query().Get("ref") ref := r.URL.Query().Get("ref")
ua := r.Header.Get("User-Agent") ua := r.Header.Get("User-Agent")
if !(r.Header.Get("DNT") == "1" && appConfig.dnt) { if !(r.Header.Get("DNT") == "1" && appConfig.Dnt) {
go app.db.trackView(url, ref, ua) // run with goroutine for awesome speed! go app.db.trackView(url, ref, ua) // run with goroutine for awesome speed!
_, _ = fmt.Fprint(w, "true") _, _ = fmt.Fprint(w, "true")
} }
@ -81,7 +83,7 @@ func HelloResponseHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = fmt.Fprint(w, "Hello from KISSS") _, _ = fmt.Fprint(w, "Hello from KISSS")
} }
func serveTrackingScript(w http.ResponseWriter, r *http.Request) { func TrackingScriptHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("Cache-Control", "public, max-age=432000") // 5 days w.Header().Set("Cache-Control", "public, max-age=432000") // 5 days
filename := "kis3.js" filename := "kis3.js"
@ -97,10 +99,10 @@ func serveTrackingScript(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, filename, stat.ModTime(), file) http.ServeContent(w, r, filename, stat.ModTime(), file)
} }
func requestStats(w http.ResponseWriter, r *http.Request) { func StatsHandler(w http.ResponseWriter, r *http.Request) {
// Require authentication // Require authentication
if appConfig.statsAuth { if appConfig.statsAuth() {
if !helpers.CheckAuth(w, r, appConfig.statsUsername, appConfig.statsPassword) { if !helpers.CheckAuth(w, r, appConfig.StatsUsername, appConfig.StatsPassword) {
return return
} }
} }

58
reports.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/jordan-wright/email"
"github.com/whiteshtef/clockwork"
"io/ioutil"
"net"
"net/http"
"net/smtp"
)
func setupReports() {
scheduler := clockwork.NewScheduler()
for _, r := range appConfig.Reports {
scheduler.Schedule().Every().Day().At(r.Time).Do(func() {
executeReport(&r)
})
}
go scheduler.Run()
}
func executeReport(r *report) {
fmt.Println("Execute report:", r.Name)
req, e := http.NewRequest("GET", "http://localhost:"+appConfig.Port+"/stats?"+r.Query, nil)
if e != nil {
fmt.Println("Executing report failed:", e)
return
}
req.SetBasicAuth(appConfig.StatsUsername, appConfig.StatsPassword)
res, e := http.DefaultClient.Do(req)
if e != nil {
fmt.Println("Executing report failed:", e)
return
}
body, e := ioutil.ReadAll(res.Body)
if e != nil {
fmt.Println("Executing report failed:", e)
return
}
sendMail(r, body)
}
func sendMail(r *report, content []byte) {
smtpHostNoPort, _, _ := net.SplitHostPort(r.SmtpHost)
mail := email.NewEmail()
mail.From = r.From
mail.To = []string{r.To}
mail.Subject = "KISSS report: " + r.Name
mail.Text = content
e := mail.Send(r.SmtpHost, smtp.PlainAuth("", r.SmtpUser, r.SmtpPassword, smtpHostNoPort))
if e != nil {
fmt.Println("Sending report failed:", e)
return
} else {
fmt.Println("Report sent")
}
}