User Auth

This commit is contained in:
aceberg 2023-08-18 13:09:53 +07:00
parent 76ae187051
commit e51c870c0c
19 changed files with 415 additions and 35 deletions

13
go.mod
View file

@ -4,15 +4,16 @@ go 1.20
require (
github.com/containrrr/shoutrrr v0.7.1
github.com/google/uuid v1.3.0
github.com/spf13/viper v1.16.0
modernc.org/sqlite v1.24.0
golang.org/x/crypto v0.12.0
modernc.org/sqlite v1.25.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/magiconair/properties v1.8.7 // indirect
@ -27,17 +28,17 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect

22
go.sum
View file

@ -610,6 +610,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -842,8 +844,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -857,8 +859,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1181,16 +1183,16 @@ modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA=
modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=

35
internal/auth/auth.go Normal file
View file

@ -0,0 +1,35 @@
package auth
import (
"net/http"
"time"
)
// Auth - main auth func
func Auth(next http.HandlerFunc, conf *Conf) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !conf.Auth || conf.User == "" || conf.Password == "" {
next.ServeHTTP(w, r)
return
}
sessionToken := getTokenFromCookie(r)
userSession, exists := allSessions[sessionToken]
if !exists {
http.Redirect(w, r, "/login/", 302)
return
}
if userSession.Before(time.Now()) {
delete(allSessions, sessionToken)
http.Redirect(w, r, "/login/", 302)
return
}
userSession = time.Now().Add(conf.Expire)
allSessions[sessionToken] = userSession
next.ServeHTTP(w, r)
}
}

22
internal/auth/cookie.go Normal file
View file

@ -0,0 +1,22 @@
package auth
import (
"net/http"
)
func setTokenCookie(w http.ResponseWriter, token string) {
cookie := http.Cookie{Name: cookieName, Value: token, Path: "/"}
http.SetCookie(w, &cookie)
}
func getTokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie(cookieName)
if err != nil {
return ""
}
return cookie.Value
}

27
internal/auth/hash.go Normal file
View file

@ -0,0 +1,27 @@
package auth
import (
"log"
"golang.org/x/crypto/bcrypt"
)
// HashPassword - generate hash from password
func HashPassword(pw string) string {
hashed, err := bcrypt.GenerateFromPassword([]byte(pw), 10)
if err != nil {
log.Println("PASSWORD ERROR:", err)
return ""
}
return string(hashed)
}
// MatchPasswords - check if password and hash matches
func MatchPasswords(hash, pw string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
return err == nil
}

View file

@ -0,0 +1,18 @@
package auth
import (
"time"
)
// Conf - auth config
type Conf struct {
Auth bool
User string
Password string
ExpStr string
Expire time.Duration
}
var allSessions = map[string]time.Time{}
var cookieName = "wyl_session_token"

33
internal/auth/session.go Normal file
View file

@ -0,0 +1,33 @@
package auth
import (
"net/http"
"time"
"github.com/google/uuid"
)
// StartSession for new login
func StartSession(w http.ResponseWriter, r *http.Request) {
sessionToken := uuid.NewString()
allSessions[sessionToken] = time.Now().Add(60 * time.Second)
setTokenCookie(w, sessionToken)
http.Redirect(w, r, "/", 302)
}
// LogOut - log out
func LogOut(w http.ResponseWriter, r *http.Request) {
sessionToken := getTokenFromCookie(r)
delete(allSessions, sessionToken)
setTokenCookie(w, "")
http.Redirect(w, r, "/", 302)
}

View file

@ -0,0 +1,46 @@
package auth
import (
"errors"
"time"
)
func timeParse(timeout string) (time.Duration, error) {
var err error
var t time.Duration
length := len(timeout)
if length > 1 {
suffix := timeout[length-1]
switch string(suffix) {
case "h":
t, err = time.ParseDuration(timeout)
case "m":
t, err = time.ParseDuration(timeout)
case "d": //day
t, err = time.ParseDuration(timeout[:length-1] + "h")
t = 24 * t
case "M": // month
t, err = time.ParseDuration(timeout[:length-1] + "h")
t = 730 * t
default:
err = errors.New("ERROR: TimeParse: wrong time format")
}
} else {
err = errors.New("ERROR: TimeParse: wrong time format")
}
return t, err
}
// ToTime - converts string (example: 3d) to time.Duration
func ToTime(s string) time.Duration {
t, err := timeParse(s)
if err != nil {
t, _ = timeParse("7d")
}
return t
}

View file

@ -3,12 +3,14 @@ package conf
import (
"github.com/spf13/viper"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// Get - get app config
func Get(path string) (config models.Conf) {
func Get(path string) (config models.Conf, authConf auth.Conf) {
viper.SetDefault("IFACE", "enp1s0")
viper.SetDefault("DBPATH", "/data/db.sqlite")
viper.SetDefault("GUIIP", "localhost")
@ -19,6 +21,9 @@ func Get(path string) (config models.Conf) {
viper.SetDefault("COLOR", "light")
viper.SetDefault("IGNOREIP", "no")
viper.SetDefault("LOGLEVEL", "verbose")
viper.SetDefault("AUTH_USER", "")
viper.SetDefault("AUTH_PASSWORD", "")
viper.SetDefault("AUTH_EXPIRE", "7d")
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
@ -37,12 +42,19 @@ func Get(path string) (config models.Conf) {
config.Color = viper.Get("COLOR").(string)
config.IgnoreIP = viper.Get("IGNOREIP").(string)
config.LogLevel = viper.Get("LOGLEVEL").(string)
authConf.Auth = viper.GetBool("AUTH")
authConf.User, _ = viper.Get("AUTH_USER").(string)
authConf.Password, _ = viper.Get("AUTH_PASSWORD").(string)
authConf.ExpStr, _ = viper.Get("AUTH_EXPIRE").(string)
return config
authConf.Expire = auth.ToTime(authConf.ExpStr)
config.Auth = authConf.Auth
return config, authConf
}
// Write - write config to file
func Write(path string, config models.Conf) {
func Write(path string, config models.Conf, authConf auth.Conf) {
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
@ -58,6 +70,11 @@ func Write(path string, config models.Conf) {
viper.Set("IGNOREIP", config.IgnoreIP)
viper.Set("LOGLEVEL", config.LogLevel)
viper.Set("auth", authConf.Auth)
viper.Set("auth_user", authConf.User)
viper.Set("auth_password", authConf.Password)
viper.Set("auth_expire", authConf.ExpStr)
err := viper.WriteConfig()
check.IfError(err)
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"log"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/notify"
@ -37,9 +38,10 @@ func ToYAML(path string) string {
}
func migrateConfig(oldPath, newPath string) {
var authConf auth.Conf
config := getOldConfig(oldPath)
conf.Write(newPath, config)
conf.Write(newPath, config, authConf)
msg := fmt.Sprintf("Config file migrated from ENV to YAML format. \nOld config file %s is no longer used and can be deleted. \nNew config file is %s, please update your settings.", oldPath, newPath)

View file

@ -1,5 +1,9 @@
package models
import (
"github.com/aceberg/WatchYourLAN/internal/auth"
)
// Host - one host
type Host struct {
ID uint16
@ -26,6 +30,7 @@ type Conf struct {
LogLevel string
NodePath string
Icon string
Auth bool
}
// GuiData - all data sent to html page
@ -34,4 +39,5 @@ type GuiData struct {
Hosts []Host
Themes []string
Version string
Auth auth.Conf
}

52
internal/web/auth-conf.go Normal file
View file

@ -0,0 +1,52 @@
package web
import (
"log"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func authConfHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
guiData.Auth.Auth = authConf.Auth
guiData.Auth.User = authConf.User
guiData.Auth.ExpStr = authConf.ExpStr
execTemplate(w, "auth", guiData)
}
func saveAuthHandler(w http.ResponseWriter, r *http.Request) {
authConf.User = r.FormValue("user")
authConf.ExpStr = r.FormValue("expire")
authStr := r.FormValue("auth")
pw := r.FormValue("password")
if authStr == "on" {
authConf.Auth = true
} else {
authConf.Auth = false
}
AppConfig.Auth = authConf.Auth
if pw != "" {
authConf.Password = auth.HashPassword(pw)
}
authConf.Expire = auth.ToTime(authConf.ExpStr)
if authConf.Auth && (authConf.User == "" || authConf.Password == "") {
log.Println("WARNING: Auth won't work with empty login or password.")
authConf.Auth = false
}
conf.Write(ConfigPath, AppConfig, authConf)
http.Redirect(w, r, r.Header.Get("Referer"), 302)
}

View file

@ -48,7 +48,7 @@ func saveConfigHandler(w http.ResponseWriter, r *http.Request) {
close(QuitScan)
conf.Write(ConfigPath, AppConfig)
conf.Write(ConfigPath, AppConfig, authConf)
log.Println("INFO: writing new config")

View file

@ -3,10 +3,13 @@ package web
import (
"embed"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/models"
)
var (
authConf auth.Conf
// AppConfig - app config
AppConfig models.Conf

31
internal/web/login.go Normal file
View file

@ -0,0 +1,31 @@
package web
import (
"log"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/models"
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
var guiData models.GuiData
guiData.Config = AppConfig
username := r.FormValue("username")
password := r.FormValue("password")
logout := r.FormValue("logout")
if username == authConf.User && auth.MatchPasswords(authConf.Password, password) {
log.Println("INFO: user '"+username+"' logged in. Session expire time", authConf.Expire)
auth.StartSession(w, r)
}
if logout == "yes" {
auth.LogOut(w, r)
}
execTemplate(w, "login", guiData)
}

View file

@ -0,0 +1,53 @@
{{ define "auth" }}
<body>
<div class="container mt-5">
<div class="row">
<div class="col">
<table class="table">
<form action="/auth_save/" method="post">
<tr>
<td>Enable</td>
<td>
<div class="form-check form-switch">
{{ if .Auth.Auth }}
<input class="form-check-input" type="checkbox" name="auth" checked>
{{ else }}
<input class="form-check-input" type="checkbox" name="auth">
{{ end }}
</div>
</td>
</tr>
<tr>
<td>Expire after</td>
<td><input name="expire" type="text" class="form-control" value="{{ .Auth.ExpStr }}"></td>
</tr>
<tr>
<td>Login</td>
<td><input name="user" type="text" class="form-control" value="{{ .Auth.User }}"></td>
</tr>
<tr>
<td>New password</td>
<td><input name="password" type="password" class="form-control"></td>
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
</tr>
</form>
</table>
</div>
<div class="col">
<div class="alert alert-info" role="alert">
<p><b>WARNING!</b> Authentication is only safe when used over <b>https</b> and with a strong password.</p>
<p><b>Expire after</b> - when user session expires. Can be set with suffixes <b>m</b>(minute), <b>h</b>(hour), <b>d</b>(day) or <b>M</b>(month). Default: <b>7d</b>.</p>
<p><b>Login</b>. For Auth to work, login and password must not be empty.</p>
<p><b>Password</b> is stored encrypted in config file. If you forgot your password, just set <b>auth_password:</b> field to empty there.</p>
</div>
</div>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -31,6 +31,14 @@
<li class="nav-item">
<a class="nav-link active" href="/config/">Config</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/auth_conf/">Auth</a>
</li>
{{ if .Config.Auth }}
<li class="nav-item">
<a class="nav-link active" href="/login/?logout=yes">Log Out</a>
</li>
{{ end }}
</ul>
<ul class="nav navbar-nav navbar-right">
<form action="/search_hosts/" method="post">

View file

@ -0,0 +1,19 @@
{{ define "login" }}
</head>
<body>
<div class="container">
<br>
<br>
<div class="row text-center">
<div class="d-flex align-items-center justify-content-center">
<form action="/login/" method="post">
<input name="username" type="text" placeholder="Login" class="form-control p-2 m-2">
<input name="password" type="password" placeholder="Password" class="form-control p-2 m-2">
<button type="submit" class="btn btn-primary p-2 m-2">Login</button>
</form>
</div>
</div>
</div>
{{ template "footer" }}
{{ end }}

View file

@ -4,6 +4,7 @@ import (
"log"
"net/http"
"github.com/aceberg/WatchYourLAN/internal/auth"
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/aceberg/WatchYourLAN/internal/db"
@ -15,7 +16,7 @@ import (
func Gui(configPath, nodePath string) {
ConfigPath = migrate.ToYAML(configPath)
AppConfig = conf.Get(ConfigPath)
AppConfig, authConf = conf.Get(ConfigPath)
AppConfig.NodePath = nodePath
AppConfig.Icon = Icon
@ -33,19 +34,23 @@ func Gui(configPath, nodePath string) {
log.Printf("Web GUI at http://%s", address)
log.Println("=================================== ")
http.HandleFunc("/", indexHandler)
http.HandleFunc("/clear/", clearHandler)
http.HandleFunc("/config/", configHandler)
http.HandleFunc("/del_host/", delHandler)
http.HandleFunc("/home/", homeHandler)
http.HandleFunc("/host/", hostHandler)
http.HandleFunc("/line/", lineHandler)
http.HandleFunc("/port_scan/", portHandler)
http.HandleFunc("/save_config/", saveConfigHandler)
http.HandleFunc("/search_hosts/", searchHandler)
http.HandleFunc("/sort_hosts/", sortHandler)
http.HandleFunc("/test_notify/", testNotifyHandler)
http.HandleFunc("/update_host/", updateHandler)
http.HandleFunc("/login/", loginHandler)
http.HandleFunc("/", auth.Auth(indexHandler, &authConf))
http.HandleFunc("/auth_conf/", auth.Auth(authConfHandler, &authConf))
http.HandleFunc("/auth_save/", auth.Auth(saveAuthHandler, &authConf))
http.HandleFunc("/clear/", auth.Auth(clearHandler, &authConf))
http.HandleFunc("/config/", auth.Auth(configHandler, &authConf))
http.HandleFunc("/del_host/", auth.Auth(delHandler, &authConf))
http.HandleFunc("/home/", auth.Auth(homeHandler, &authConf))
http.HandleFunc("/host/", auth.Auth(hostHandler, &authConf))
http.HandleFunc("/line/", auth.Auth(lineHandler, &authConf))
http.HandleFunc("/port_scan/", auth.Auth(portHandler, &authConf))
http.HandleFunc("/save_config/", auth.Auth(saveConfigHandler, &authConf))
http.HandleFunc("/search_hosts/", auth.Auth(searchHandler, &authConf))
http.HandleFunc("/sort_hosts/", auth.Auth(sortHandler, &authConf))
http.HandleFunc("/test_notify/", auth.Auth(testNotifyHandler, &authConf))
http.HandleFunc("/update_host/", auth.Auth(updateHandler, &authConf))
err := http.ListenAndServe(address, nil)
check.IfError(err)
}