mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2024-12-14 18:51:34 +03:00
+ Login page and web sessions
+ /control/login + /control/logout
This commit is contained in:
parent
74381b0cad
commit
6304a7b91b
@ -50,6 +50,9 @@ Contents:
|
||||
* API: Get filtering parameters
|
||||
* API: Set filtering parameters
|
||||
* API: Set URL parameters
|
||||
* Log-in page
|
||||
* API: Log in
|
||||
* API: Log out
|
||||
|
||||
|
||||
## Relations between subsystems
|
||||
@ -1097,3 +1100,82 @@ Request:
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
|
||||
## Log-in page
|
||||
|
||||
After user completes the steps of installation wizard, he must log in into dashboard using his name and password. After user successfully logs in, he gets the Cookie which allows the server to authenticate him next time without password. After the Cookie is expired, user needs to perform log-in operation again. All requests without a proper Cookie get redirected to Log-In page with prompt for name and password.
|
||||
|
||||
YAML configuration:
|
||||
|
||||
users:
|
||||
- name: "..."
|
||||
password: "..." // bcrypt hash
|
||||
...
|
||||
|
||||
|
||||
Session DB file:
|
||||
|
||||
session="..." expire=123456
|
||||
...
|
||||
|
||||
Session data is SHA(random()+name+password).
|
||||
Expiration time is UNIX time when cookie gets expired.
|
||||
|
||||
Any request to server must come with Cookie header:
|
||||
|
||||
GET /...
|
||||
Cookie: session=...
|
||||
|
||||
If not authenticated, server sends a redirect response:
|
||||
|
||||
302 Found
|
||||
Location: /login.html
|
||||
|
||||
|
||||
### Reset password
|
||||
|
||||
There is no mechanism to reset the password. Instead, the administrator must use `htpasswd` utility to generate a new hash:
|
||||
|
||||
htpasswd -B -n -b username password
|
||||
|
||||
It will print `username:<HASH>` to the terminal. `<HASH>` value may be used in AGH YAML configuration file as a value to `password` setting:
|
||||
|
||||
users:
|
||||
- name: "..."
|
||||
password: <HASH>
|
||||
|
||||
|
||||
|
||||
### API: Log in
|
||||
|
||||
Perform a log-in operation for administrator. Server generates a session for this name+password pair, stores it in file. UI needs to perform all requests with this value inside Cookie HTTP header.
|
||||
|
||||
Request:
|
||||
|
||||
POST /control/login
|
||||
|
||||
{
|
||||
name: "..."
|
||||
password: "..."
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
Set-Cookie: session=...; Expires=Wed, 09 Jun 2021 10:18:14 GMT; Path=/; HttpOnly
|
||||
|
||||
|
||||
### API: Log out
|
||||
|
||||
Perform a log-out operation for administrator. Server removes the session from its DB and sets an expired cookie value.
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/logout
|
||||
|
||||
Response:
|
||||
|
||||
302 Found
|
||||
Location: /login.html
|
||||
Set-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT
|
||||
|
3
go.mod
3
go.mod
@ -18,7 +18,8 @@ require (
|
||||
github.com/miekg/dns v1.1.8
|
||||
github.com/sparrc/go-ping v0.0.0-20181106165434-ef3ab45e41b0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
go.etcd.io/bbolt v1.3.3 // indirect
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
|
408
home/auth.go
Normal file
408
home/auth.go
Normal file
@ -0,0 +1,408 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const cookieTTL = 365 * 24 // in hours
|
||||
const expireTime = 30 * 24 // in hours
|
||||
|
||||
// Auth - global object
|
||||
type Auth struct {
|
||||
db *bbolt.DB
|
||||
sessions map[string]uint32 // session -> expiration time (in seconds)
|
||||
lock sync.Mutex
|
||||
users []User
|
||||
}
|
||||
|
||||
// User object
|
||||
type User struct {
|
||||
Name string `yaml:"name"`
|
||||
PasswordHash string `yaml:"password"` // bcrypt hash
|
||||
}
|
||||
|
||||
// InitAuth - create a global object
|
||||
func InitAuth(dbFilename string, users []User) *Auth {
|
||||
a := Auth{}
|
||||
a.sessions = make(map[string]uint32)
|
||||
rand.Seed(time.Now().UTC().Unix())
|
||||
var err error
|
||||
a.db, err = bbolt.Open(dbFilename, 0644, nil)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Open: %s", err)
|
||||
return nil
|
||||
}
|
||||
a.loadSessions()
|
||||
a.users = users
|
||||
log.Debug("Auth: initialized. users:%d sessions:%d", len(a.users), len(a.sessions))
|
||||
return &a
|
||||
}
|
||||
|
||||
// Close - close module
|
||||
func (a *Auth) Close() {
|
||||
_ = a.db.Close()
|
||||
}
|
||||
|
||||
// load sessions from file, remove expired sessions
|
||||
func (a *Auth) loadSessions() {
|
||||
tx, err := a.db.Begin(true)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Begin: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte("sessions"))
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
removed := 0
|
||||
now := uint32(time.Now().UTC().Unix())
|
||||
forEach := func(k, v []byte) error {
|
||||
i := binary.BigEndian.Uint32(v)
|
||||
if i <= now {
|
||||
err = bkt.Delete(k)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Delete: %s", err)
|
||||
} else {
|
||||
removed++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
a.sessions[hex.EncodeToString(k)] = i
|
||||
return nil
|
||||
}
|
||||
_ = bkt.ForEach(forEach)
|
||||
if removed != 0 {
|
||||
tx.Commit()
|
||||
}
|
||||
log.Debug("Auth: loaded %d sessions from DB (removed %d expired)", len(a.sessions), removed)
|
||||
}
|
||||
|
||||
// store session data in file
|
||||
func (a *Auth) storeSession(data []byte, expire uint32) {
|
||||
a.lock.Lock()
|
||||
a.sessions[hex.EncodeToString(data)] = expire
|
||||
a.lock.Unlock()
|
||||
|
||||
tx, err := a.db.Begin(true)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Begin: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.CreateBucketIfNotExists: %s", err)
|
||||
return
|
||||
}
|
||||
var val []byte
|
||||
val = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(val, expire)
|
||||
err = bkt.Put(data, val)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Put: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Commit: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Auth: stored session in DB")
|
||||
}
|
||||
|
||||
// remove session from file
|
||||
func (a *Auth) removeSession(sess []byte) {
|
||||
tx, err := a.db.Begin(true)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Begin: %s", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte("sessions"))
|
||||
if bkt == nil {
|
||||
log.Error("Auth: bbolt.Bucket")
|
||||
return
|
||||
}
|
||||
err = bkt.Delete(sess)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Put: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Commit: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("Auth: removed session from DB")
|
||||
}
|
||||
|
||||
// CheckSession - check if session is valid
|
||||
// Return 0 if OK; -1 if session doesn't exist; 1 if session has expired
|
||||
func (a *Auth) CheckSession(sess string) int {
|
||||
now := uint32(time.Now().UTC().Unix())
|
||||
update := false
|
||||
|
||||
a.lock.Lock()
|
||||
expire, ok := a.sessions[sess]
|
||||
if !ok {
|
||||
a.lock.Unlock()
|
||||
return -1
|
||||
}
|
||||
if expire <= now {
|
||||
delete(a.sessions, sess)
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.removeSession(key)
|
||||
a.lock.Unlock()
|
||||
return 1
|
||||
}
|
||||
|
||||
newExpire := now + expireTime*60*60
|
||||
if expire/(24*60*60) != newExpire/(24*60*60) {
|
||||
// update expiration time once a day
|
||||
update = true
|
||||
a.sessions[sess] = newExpire
|
||||
}
|
||||
|
||||
a.lock.Unlock()
|
||||
|
||||
if update {
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.storeSession(key, expire)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// RemoveSession - remove session
|
||||
func (a *Auth) RemoveSession(sess string) {
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.lock.Lock()
|
||||
delete(a.sessions, sess)
|
||||
a.lock.Unlock()
|
||||
a.removeSession(key)
|
||||
}
|
||||
|
||||
type loginJSON struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func getSession(u *User) []byte {
|
||||
d := []byte(fmt.Sprintf("%d%s%s", rand.Uint32(), u.Name, u.PasswordHash))
|
||||
hash := sha256.Sum256(d)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
req := loginJSON{}
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusBadRequest, "json decode: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
u := config.auth.UserFind(req.Name, req.Password)
|
||||
if len(u.Name) == 0 {
|
||||
time.Sleep(1 * time.Second)
|
||||
httpError(w, http.StatusBadRequest, "invalid login or password")
|
||||
return
|
||||
}
|
||||
|
||||
sess := getSession(&u)
|
||||
|
||||
now := time.Now().UTC()
|
||||
expire := now.Add(cookieTTL * time.Hour)
|
||||
expstr := expire.Format(time.RFC1123)
|
||||
expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT"
|
||||
expstr += "GMT"
|
||||
|
||||
expireSess := uint32(now.Unix()) + expireTime*60*60
|
||||
config.auth.storeSession(sess, expireSess)
|
||||
|
||||
s := fmt.Sprintf("session=%s; Path=/; HttpOnly; Expires=%s", hex.EncodeToString(sess), expstr)
|
||||
w.Header().Set("Set-Cookie", s)
|
||||
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
returnOK(w)
|
||||
}
|
||||
|
||||
func handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
cookie := r.Header.Get("Cookie")
|
||||
sess := parseCookie(cookie)
|
||||
|
||||
config.auth.RemoveSession(sess)
|
||||
|
||||
w.Header().Set("Location", "/login.html")
|
||||
|
||||
s := fmt.Sprintf("session=; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT")
|
||||
w.Header().Set("Set-Cookie", s)
|
||||
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
// RegisterAuthHandlers - register handlers
|
||||
func RegisterAuthHandlers() {
|
||||
http.Handle("/control/login", postInstallHandler(ensureHandler("POST", handleLogin)))
|
||||
httpRegister("GET", "/control/logout", handleLogout)
|
||||
}
|
||||
|
||||
func parseCookie(cookie string) string {
|
||||
pairs := strings.Split(cookie, ";")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
kv := strings.SplitN(pair, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
if kv[0] == "session" {
|
||||
return kv[1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if r.URL.Path == "/login.html" {
|
||||
// redirect to dashboard if already authenticated
|
||||
authRequired := config.auth != nil && config.auth.AuthRequired()
|
||||
cookie, err := r.Cookie("session")
|
||||
if authRequired && err == nil {
|
||||
r := config.auth.CheckSession(cookie.Value)
|
||||
if r == 0 {
|
||||
w.Header().Set("Location", "/")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
} else if r < 0 {
|
||||
log.Debug("Auth: invalid cookie value: %s", cookie)
|
||||
}
|
||||
}
|
||||
|
||||
} else if r.URL.Path == "/favicon.png" ||
|
||||
strings.HasPrefix(r.URL.Path, "/login.") {
|
||||
// process as usual
|
||||
|
||||
} else if config.auth != nil && config.auth.AuthRequired() {
|
||||
// redirect to login page if not authenticated
|
||||
ok := false
|
||||
cookie, err := r.Cookie("session")
|
||||
if err == nil {
|
||||
r := config.auth.CheckSession(cookie.Value)
|
||||
if r == 0 {
|
||||
ok = true
|
||||
} else if r < 0 {
|
||||
log.Debug("Auth: invalid cookie value: %s", cookie)
|
||||
}
|
||||
} else {
|
||||
// there's no Cookie, check Basic authentication
|
||||
user, pass, ok2 := r.BasicAuth()
|
||||
if ok2 {
|
||||
u := config.auth.UserFind(user, pass)
|
||||
if len(u.Name) != 0 {
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
w.Header().Set("Location", "/login.html")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
optionalAuth(a.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
|
||||
// UserAdd - add new user
|
||||
func (a *Auth) UserAdd(u *User, password string) {
|
||||
if len(password) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Error("bcrypt.GenerateFromPassword: %s", err)
|
||||
return
|
||||
}
|
||||
u.PasswordHash = string(hash)
|
||||
|
||||
a.lock.Lock()
|
||||
a.users = append(a.users, *u)
|
||||
a.lock.Unlock()
|
||||
|
||||
log.Debug("Auth: added user: %s", u.Name)
|
||||
}
|
||||
|
||||
// UserFind - find a user
|
||||
func (a *Auth) UserFind(login string, password string) User {
|
||||
a.lock.Lock()
|
||||
defer a.lock.Unlock()
|
||||
for _, u := range a.users {
|
||||
if u.Name == login &&
|
||||
bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) == nil {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return User{}
|
||||
}
|
||||
|
||||
// GetUsers - get users
|
||||
func (a *Auth) GetUsers() []User {
|
||||
a.lock.Lock()
|
||||
users := a.users
|
||||
a.lock.Unlock()
|
||||
return users
|
||||
}
|
||||
|
||||
// AuthRequired - if authentication is required
|
||||
func (a *Auth) AuthRequired() bool {
|
||||
a.lock.Lock()
|
||||
r := (len(a.users) != 0)
|
||||
a.lock.Unlock()
|
||||
return r
|
||||
}
|
56
home/auth_test.go
Normal file
56
home/auth_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
fn := "./sessions.db"
|
||||
users := []User{
|
||||
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
|
||||
}
|
||||
|
||||
os.Remove(fn)
|
||||
config.ourWorkingDir = "."
|
||||
a := InitAuth(fn, users)
|
||||
|
||||
assert.True(t, a.CheckSession("notfound") == -1)
|
||||
a.RemoveSession("notfound")
|
||||
|
||||
sess := getSession(&users[0])
|
||||
sessStr := hex.EncodeToString(sess)
|
||||
|
||||
// check expiration
|
||||
a.storeSession(sess, uint32(time.Now().UTC().Unix()))
|
||||
assert.True(t, a.CheckSession(sessStr) == 1)
|
||||
|
||||
// add session with TTL = 2 sec
|
||||
a.storeSession(sess, uint32(time.Now().UTC().Unix()+2))
|
||||
assert.True(t, a.CheckSession(sessStr) == 0)
|
||||
|
||||
a.Close()
|
||||
|
||||
// load saved session
|
||||
a = InitAuth(fn, users)
|
||||
|
||||
// the session is still alive
|
||||
assert.True(t, a.CheckSession(sessStr) == 0)
|
||||
a.Close()
|
||||
|
||||
u := a.UserFind("name", "password")
|
||||
assert.True(t, len(u.Name) != 0)
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// load and remove expired sessions
|
||||
a = InitAuth(fn, users)
|
||||
assert.True(t, a.CheckSession(sessStr) == -1)
|
||||
|
||||
a.Close()
|
||||
os.Remove(fn)
|
||||
}
|
@ -65,13 +65,14 @@ type configuration struct {
|
||||
runningAsService bool
|
||||
disableUpdate bool // If set, don't check for updates
|
||||
appSignalChannel chan os.Signal
|
||||
clients clientsContainer
|
||||
clients clientsContainer // per-client-settings module
|
||||
controlLock sync.Mutex
|
||||
transport *http.Transport
|
||||
client *http.Client
|
||||
stats stats.Stats
|
||||
queryLog querylog.QueryLog
|
||||
filteringStarted bool
|
||||
stats stats.Stats // statistics module
|
||||
queryLog querylog.QueryLog // query log module
|
||||
filteringStarted bool // TRUE if filtering module is started
|
||||
auth *Auth // HTTP authentication module
|
||||
|
||||
// cached version.json to avoid hammering github.io for each page reload
|
||||
versionCheckJSON []byte
|
||||
@ -85,8 +86,7 @@ type configuration struct {
|
||||
|
||||
BindHost string `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
||||
AuthName string `yaml:"auth_name"` // AuthName is the basic auth username
|
||||
AuthPass string `yaml:"auth_pass"` // AuthPass is the basic auth password
|
||||
Users []User `yaml:"users"` // Users that can access HTTP server
|
||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
||||
RlimitNoFile uint `yaml:"rlimit_nofile"` // Maximum number of opened fd's per process (0: default)
|
||||
|
||||
@ -352,6 +352,10 @@ func (c *configuration) write() error {
|
||||
config.Clients = append(config.Clients, cy)
|
||||
}
|
||||
|
||||
if config.auth != nil {
|
||||
config.Users = config.auth.GetUsers()
|
||||
}
|
||||
|
||||
configFile := config.getConfigFilename()
|
||||
log.Debug("Writing YAML file: %s", configFile)
|
||||
yamlText, err := yaml.Marshal(&config)
|
||||
|
@ -570,6 +570,7 @@ func registerControlHandlers() {
|
||||
RegisterBlockedServicesHandlers()
|
||||
RegisterQueryLogHandlers()
|
||||
RegisterStatsHandlers()
|
||||
RegisterAuthHandlers()
|
||||
|
||||
http.HandleFunc("/dns-query", postInstall(handleDOH))
|
||||
}
|
||||
|
@ -183,8 +183,6 @@ func copyInstallSettings(dst *configuration, src *configuration) {
|
||||
dst.BindPort = src.BindPort
|
||||
dst.DNS.BindHost = src.DNS.BindHost
|
||||
dst.DNS.Port = src.DNS.Port
|
||||
dst.AuthName = src.AuthName
|
||||
dst.AuthPass = src.AuthPass
|
||||
}
|
||||
|
||||
// Apply new configuration, start DNS server, restart Web server
|
||||
@ -237,8 +235,6 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
config.BindPort = newSettings.Web.Port
|
||||
config.DNS.BindHost = newSettings.DNS.IP
|
||||
config.DNS.Port = newSettings.DNS.Port
|
||||
config.AuthName = newSettings.Username
|
||||
config.AuthPass = newSettings.Password
|
||||
|
||||
dnsBaseDir := filepath.Join(config.ourWorkingDir, dataDir)
|
||||
initDNSServer(dnsBaseDir)
|
||||
@ -251,6 +247,10 @@ func handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
u := User{}
|
||||
u.Name = newSettings.Username
|
||||
config.auth.UserAdd(&u, newSettings.Password)
|
||||
|
||||
err = config.write()
|
||||
if err != nil {
|
||||
config.firstRun = true
|
||||
|
@ -51,6 +51,10 @@ func initDNSServer(baseDir string) {
|
||||
config.queryLog = querylog.New(conf)
|
||||
config.dnsServer = dnsforward.NewServer(config.stats, config.queryLog)
|
||||
|
||||
sessFilename := filepath.Join(config.ourWorkingDir, "data/sessions.db")
|
||||
config.auth = InitAuth(sessFilename, config.Users)
|
||||
config.Users = nil
|
||||
|
||||
initRDNS()
|
||||
initFiltering()
|
||||
}
|
||||
@ -202,6 +206,6 @@ func stopDNSServer() error {
|
||||
|
||||
config.stats.Close()
|
||||
config.queryLog.Close()
|
||||
|
||||
config.auth.Close()
|
||||
return nil
|
||||
}
|
||||
|
@ -68,35 +68,6 @@ func ensureHandler(method string, handler func(http.ResponseWriter, *http.Reques
|
||||
return &h
|
||||
}
|
||||
|
||||
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if config.AuthName == "" || config.AuthPass == "" {
|
||||
handler(w, r)
|
||||
return
|
||||
}
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != config.AuthName || pass != config.AuthPass {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
type authHandler struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
optionalAuth(a.handler.ServeHTTP)(w, r)
|
||||
}
|
||||
|
||||
func optionalAuthHandler(handler http.Handler) http.Handler {
|
||||
return &authHandler{handler}
|
||||
}
|
||||
|
||||
// -------------------
|
||||
// first run / install
|
||||
// -------------------
|
||||
|
@ -11,3 +11,12 @@ The easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and
|
||||
1. `yarn install`
|
||||
2. `yarn start`
|
||||
3. Open `http://localhost:4000/`
|
||||
|
||||
|
||||
### Authentication
|
||||
|
||||
If AdGuard Home's web user is password-protected, a web client must use authentication mechanism when sending requests to server. Basic access authentication is the most simple method - a client must pass `Authorization` HTTP header along with all requests:
|
||||
|
||||
Authorization: Basic BASE64_DATA
|
||||
|
||||
where BASE64_DATA is base64-encoded data for `username:password` string.
|
||||
|
Loading…
Reference in New Issue
Block a user