localized assets - updates for stability

pull/10/head v0.28.4
Hunter Long 2018-06-30 20:54:28 -07:00
parent 1aef1948d4
commit c20cde567e
21 changed files with 202 additions and 114 deletions

View File

@ -18,7 +18,7 @@ services:
env:
global:
- VERSION=0.28.3
- VERSION=0.28.4
- DB_HOST=localhost
- DB_USER=travis
- DB_PASS=

View File

@ -1,6 +1,6 @@
FROM alpine:latest
ENV VERSION=v0.28.3
ENV VERSION=v0.28.4
RUN apk --no-cache add libstdc++ ca-certificates
RUN wget -q https://github.com/hunterlong/statup/releases/download/$VERSION/statup-linux-alpine.tar.gz && \
@ -20,9 +20,6 @@ RUN printf "#!/usr/bin/env sh\n\$1\n" > $CMD_FILE && \
chmod +x $CMD_FILE
WORKDIR /app
#COPY build/statup-linux-alpine /usr/local/bin/statup
VOLUME /app
EXPOSE 8080
ENTRYPOINT statup

3
cli.go
View File

@ -16,6 +16,8 @@ func CatchCLI(args []string) {
core.CreateAllAssets()
case "sass":
core.CompileSASS()
case "api":
HelpEcho()
case "export":
var err error
fmt.Printf("Statup v%v Exporting Static 'index.html' page...\n", VERSION)
@ -84,6 +86,7 @@ func HelpEcho() {
fmt.Println(" statup - Main command to run Statup server")
fmt.Println(" statup version - Returns the current version of Statup")
fmt.Println(" statup run - Check all service 1 time and then quit")
fmt.Println(" statup assets - Export all assets used locally to be edited.")
fmt.Println(" statup env - Show all environment variables being used for Statup")
fmt.Println(" statup export - Exports the index page as a static HTML for pushing")
fmt.Println(" to Github Pages or your own FTP server. Export will")

View File

@ -96,6 +96,10 @@ func CreateAllAssets() {
CopyToPublic(JsBox, "js", "jquery-3.3.1.slim.min.js")
CopyToPublic(JsBox, "js", "main.js")
CopyToPublic(JsBox, "js", "setup.js")
CopyToPublic(JsBox, "js", "setup.js")
CopyToPublic(TmplBox, "", "robots.txt")
CopyToPublic(TmplBox, "", "favicon.ico")
utils.Log(1, "Compiling CSS from SCSS style...")
err := CompileSASS()
if err != nil {

View File

@ -5,7 +5,9 @@ import (
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"io/ioutil"
"net"
"net/http"
"net/url"
"regexp"
"time"
)
@ -33,7 +35,28 @@ func (s *Service) CheckQueue() {
time.Sleep(time.Duration(s.Interval) * time.Second)
}
func (s *Service) DNSCheck() (float64, error) {
t1 := time.Now()
url, err := url.Parse(s.Domain)
if err != nil {
return 0, err
}
_, err = net.LookupIP(url.Host)
if err != nil {
return 0, err
}
t2 := time.Now()
subTime := t2.Sub(t1).Seconds()
return subTime, err
}
func (s *Service) Check() *Service {
dnsLookup, err := s.DNSCheck()
if err != nil {
s.Failure(fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
return s
}
s.dnsLookup = dnsLookup
t1 := time.Now()
client := http.Client{
Timeout: 30 * time.Second,

View File

@ -10,16 +10,16 @@ type PluginJSON types.PluginJSON
type PluginRepos types.PluginRepos
type Core struct {
Name string `db:"name"`
Description string `db:"description"`
Config string `db:"config"`
ApiKey string `db:"api_key"`
ApiSecret string `db:"api_secret"`
Style string `db:"style"`
Footer string `db:"footer"`
Domain string `db:"domain"`
Version string `db:"version"`
Services []*Service
Name string `db:"name" json:"name"`
Description string `db:"description" json:"name"`
Config string `db:"config" json:"-"`
ApiKey string `db:"api_key" json:"-"`
ApiSecret string `db:"api_secret" json:"-"`
Style string `db:"style" json:"-"`
Footer string `db:"footer" json:"-"`
Domain string `db:"domain" json:"domain,omitempty"`
Version string `db:"version" json:"version,omitempty"`
Services []*Service `json:"services,omitempty"`
Plugins []plugin.Info
Repos []PluginJSON
//PluginFields []PluginSelect

View File

@ -94,6 +94,10 @@ func (f *Failure) ParseError() string {
if err {
return fmt.Sprintf("SSL Certificate invalid")
}
err = strings.Contains(f.Issue, "Client.Timeout exceeded while awaiting headers")
if err {
return fmt.Sprintf("Connection Timed Out")
}
err = strings.Contains(f.Issue, "no such host")
if err {
return fmt.Sprintf("Domain is offline or not found")
@ -106,5 +110,13 @@ func (f *Failure) ParseError() string {
if err {
return fmt.Sprintf("Connection Failed")
}
err = strings.Contains(f.Issue, "can't assign requested address")
if err {
return fmt.Sprintf("Unable to Request Address")
}
err = strings.Contains(f.Issue, "no route to host")
if err {
return fmt.Sprintf("Domain is offline or not found")
}
return f.Issue
}

View File

@ -36,6 +36,7 @@ type Service struct {
LastResponse string
LastStatusCode int
LastOnline time.Time
dnsLookup float64 `json:"dns_lookup_time"`
}
func serviceCol() db.Collection {
@ -194,7 +195,12 @@ func (u *Service) Delete() error {
return err
}
func (u *Service) Update() {
func (u *Service) Update(s *Service) {
res := serviceCol().Find("id", u.Id)
err := res.Update(s)
if err != nil {
utils.Log(3, fmt.Sprintf("Failed to update service %v. %v", u.Name, err))
}
OnUpdateService(u)
}

View File

@ -9,10 +9,18 @@ import (
)
func ApiIndexHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
json.NewEncoder(w).Encode(core.CoreApp)
}
func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
checkin := core.FindCheckin(vars["api"])
checkin.Receivehit()
@ -21,32 +29,63 @@ func ApiCheckinHandler(w http.ResponseWriter, r *http.Request) {
}
func ApiServiceHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"]))
json.NewEncoder(w).Encode(service)
}
func ApiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"]))
var s core.Service
decoder := json.NewDecoder(r.Body)
decoder.Decode(&s)
json.NewEncoder(w).Encode(service)
service.Update(&s)
json.NewEncoder(w).Encode(s)
}
func ApiAllServicesHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
services, _ := core.SelectAllServices()
json.NewEncoder(w).Encode(services)
}
func ApiUserHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
user, _ := core.SelectUser(utils.StringInt(vars["id"]))
json.NewEncoder(w).Encode(user)
}
func ApiAllUsersHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
users, _ := core.SelectAllUsers()
json.NewEncoder(w).Encode(users)
}
func isAPIAuthorized(r *http.Request) bool {
if IsAuthenticated(r) {
return true
}
if isAuthorized(r) {
return true
}
return false
}

View File

@ -14,8 +14,7 @@ type dashboard struct {
}
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
auth := IsAuthenticated(r)
if !auth {
if !IsAuthenticated(r) {
err := core.ErrorResponse{}
ExecuteResponse(w, r, "login.html", err)
} else {

View File

@ -1,28 +1,10 @@
package handlers
import (
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/utils"
"net/http"
)
func RobotsTxtHandler(w http.ResponseWriter, r *http.Request) {
robots := []byte(`User-agent: *
Disallow: /login
Disallow: /dashboard
Host: ` + core.CoreApp.Domain)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(robots))
}
func FavIconHandler(w http.ResponseWriter, r *http.Request) {
data, err := core.TmplBox.String("favicon.ico")
if err != nil {
utils.Log(2, err)
}
w.Header().Set("Content-Type", "image/x-icon")
w.WriteHeader(http.StatusOK)
w.Write([]byte(data))
func Error404Handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
ExecuteResponse(w, r, "error_404.html", nil)
}

View File

@ -8,24 +8,26 @@ import (
"strings"
)
//
// Use your Statup Secret API Key for Authentication
//
// scrape_configs:
// - job_name: 'statup'
// bearer_token: MY API SECRET HERE
// static_configs:
// - targets: ['statup:8080']
//
func PrometheusHandler(w http.ResponseWriter, r *http.Request) {
utils.Log(1, fmt.Sprintf("Prometheus /metrics Request From IP: %v\n", r.RemoteAddr))
var token string
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token != core.CoreApp.ApiSecret {
if !isAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
metrics := []string{}
system := fmt.Sprintf("statup_total_failures %v\n", core.CountFailures())
system += fmt.Sprintf("statup_total_services %v", len(core.CoreApp.Services))
metrics = append(metrics, system)
for _, v := range core.CoreApp.Services {
online := 1
if !v.Online {
@ -42,3 +44,16 @@ func PrometheusHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(output))
}
func isAuthorized(r *http.Request) bool {
var token string
tokens, ok := r.Header["Authorization"]
if ok && len(tokens) >= 1 {
token = tokens[0]
token = strings.TrimPrefix(token, "Bearer ")
}
if token == core.CoreApp.ApiSecret {
return true
}
return false
}

View File

@ -10,17 +10,7 @@ import (
func Router() *mux.Router {
r := mux.NewRouter()
r.Handle("/", http.HandlerFunc(IndexHandler))
if core.UsingAssets {
cssHandler := http.FileServer(http.Dir("./assets/css"))
jsHandler := http.FileServer(http.Dir("./assets/js"))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", cssHandler))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", jsHandler))
} else {
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(core.CssBox.HTTPBox())))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(core.JsBox.HTTPBox())))
}
r.Handle("/robots.txt", http.HandlerFunc(RobotsTxtHandler)).Methods("GET")
r.Handle("/favicon.ico", http.HandlerFunc(FavIconHandler)).Methods("GET")
LocalizedAssets(r)
r.Handle("/setup", http.HandlerFunc(SetupHandler)).Methods("GET")
r.Handle("/setup", http.HandlerFunc(ProcessSetupHandler)).Methods("POST")
r.Handle("/dashboard", http.HandlerFunc(DashboardHandler)).Methods("GET")
@ -54,6 +44,25 @@ func Router() *mux.Router {
r.Handle("/api/users", http.HandlerFunc(ApiAllUsersHandler))
r.Handle("/api/users/{id}", http.HandlerFunc(ApiUserHandler))
r.Handle("/metrics", http.HandlerFunc(PrometheusHandler))
r.NotFoundHandler = http.HandlerFunc(Error404Handler)
Store = sessions.NewCookieStore([]byte("secretinfo"))
return r
}
func LocalizedAssets(r *mux.Router) *mux.Router {
if core.UsingAssets {
cssHandler := http.FileServer(http.Dir("./assets/css"))
jsHandler := http.FileServer(http.Dir("./assets/js"))
indexHandler := http.FileServer(http.Dir("./assets/"))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", cssHandler))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", jsHandler))
r.PathPrefix("/robots.txt").Handler(indexHandler)
r.PathPrefix("/favicon.ico").Handler(indexHandler)
} else {
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(core.CssBox.HTTPBox())))
r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(core.JsBox.HTTPBox())))
r.PathPrefix("/robots.txt").Handler(http.FileServer(core.TmplBox.HTTPBox()))
r.PathPrefix("/favicon.ico").Handler(http.FileServer(core.TmplBox.HTTPBox()))
}
return r
}

View File

@ -105,7 +105,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port"))
checkType := r.PostForm.Get("check_type")
service = &core.Service{
serviceUpdate := &core.Service{
Name: name,
Domain: domain,
Method: method,
@ -115,7 +115,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
Type: checkType,
Port: port,
}
service.Update()
service.Update(serviceUpdate)
ExecuteResponse(w, r, "service.html", service)
}

View File

@ -48,7 +48,7 @@ HTML, BODY {
font-size: 2.3rem;
font-weight: bold;
display: block;
color: #47d337; }
color: #4f4f4f; }
.stats_area {
text-align: center;

View File

@ -8,7 +8,7 @@ $description-color: #939393;
$service-background: #ffffff;
$service-border: 1px solid rgba(0,0,0,.125);
$service-title: #444444;
$service-stats-color: #47d337;
$service-stats-color: #4f4f4f;
$service-description-color: #fff;
$service-stats-size: 2.3rem;

View File

@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1.0, user-scalable=0">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/base.css">
<title>Statup | Page Not Found</title>
</head>
<body>
<div class="container col-md-7 col-sm-12 mt-md-5 bg-light">
<div class="col-12 mt-3">
<div class="alert alert-danger" role="alert">
Sorry, this page doesn't seem to exist.
</div>
</div>
</div>
{{template "footer"}}
<script src="/js/jquery-3.3.1.slim.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -3,6 +3,6 @@
{{ if .Core.Footer }}
{{ safe .Core.Footer }}
{{ end }}
<a href="https://statup.io" target="_blank">Statup {{ VERSION }} made with ❤️</a> | <a href="/dashboard">Dashboard</a>
<a href="https://github.com/hunterlong/statup" target="_blank">Statup {{ VERSION }} made with ❤️</a> | <a href="/dashboard">Dashboard</a>
</div>
{{ end }}

View File

@ -1,35 +0,0 @@
{{define "slack"}}
<form action="/plugins/save_slack" method="POST">
<div class="form-group row">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1">
<label class="custom-control-label" for="customCheck1">Slack Integration Enabled</label>
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">API Key</label>
<div class="col-sm-8">
<input type="text" name="domain" class="form-control" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">API Secret</label>
<div class="col-sm-8">
<input type="text" name="expected" class="form-control" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<label for="inputPassword3" class="col-sm-4 col-form-label">Channel</label>
<div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" id="inputPassword3">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-success">Save</button>
</div>
</div>
</form>
{{end}}

3
source/tmpl/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Disallow: /login
Disallow: /dashboard

View File

@ -23,11 +23,11 @@ type Hit struct {
}
type Failure struct {
Id int `db:"id,omitempty"`
Issue string `db:"issue"`
Method string `db:"method"`
Service int64 `db:"service"`
CreatedAt time.Time `db:"created_at"`
Id int `db:"id,omitempty" json:"id"`
Issue string `db:"issue" json:"issue"`
Method string `db:"method" json:"method,omitempty"`
Service int64 `db:"service" json:"service_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type Checkin struct {
@ -43,17 +43,17 @@ type Checkin struct {
type Communication struct {
Id int64 `db:"id,omitempty" json:"id"`
Method string `db:"method" json:"method"`
Host string `db:"host" json:"host"`
Port int `db:"port" json:"port"`
Username string `db:"username" json:"user"`
Host string `db:"host" json:"-"`
Port int `db:"port" json:"-"`
Username string `db:"username" json:"-"`
Password string `db:"password" json:"-"`
Var1 string `db:"var1" json:"var1"`
Var2 string `db:"var2" json:"var2"`
ApiKey string `db:"api_key" json:"api_key"`
ApiSecret string `db:"api_secret" json:"api_secret"`
Var1 string `db:"var1" json:"-"`
Var2 string `db:"var2" json:"-"`
ApiKey string `db:"api_key" json:"-"`
ApiSecret string `db:"api_secret" json:"-"`
Enabled bool `db:"enabled" json:"enabled"`
Limits int64 `db:"limits" json:"limits"`
Removable bool `db:"removable" json:"removable"`
Limits int64 `db:"limits" json:"-"`
Removable bool `db:"removable" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}