continuous-integration/drone/push Build encountered an error
Go: - staticcheck 399 -> 0 with staticcheck.conf (style checks ST1000/1003/ 1005/1016/1020/1021/1022 excluded; error strings are shown in the UI). - io/ioutil and rand.Seed removed; CloseNotifier kept with a lint-ignore until the Phase 2 context rewrite. - Dead code deleted: Auto handler, getStreamByChannelID, updateXEPG, indexOfInt, jsonToMapInt64, removeOldSystemData, randomTime, and the commented-out blocks in struct-buffer.go and internal/authentication. - Duplicates folded: cacheImagesInBackground(), one addErrorToStream(). - Bugs found by SA4006/SA5001: os.Create handle leaked per ffmpeg segment (buffer.go), http.NewRequest error unchecked (buffer.go), xepg.json migration wrote null on read error (migrate.go), WriteUserData errors silently dropped (authentication.go), defer Close before error check (buffer.go, toolchain.go). checkFilePermission results were discarded; an unwritable config or temp dir is now fatal at start-up. - gofmt applied repo-wide; Drone runs gofmt check and staticcheck. Docker: - Entrypoint starts as root, applies PUID/PGID (falls back to XTEVE_UID/ XTEVE_GID, then image defaults), fixes config ownership only when it differs, then drops to xteve via su-exec. --user starts skip all of it. - /xteve removed from LEGACY_CONFIG_DIRS (it is the parent of the default). - mwader/static-ffmpeg pinned to 7.1.1; VOLUME /xteve/config. - Compose files pull registry.coadcorp.com/nathan/xteve:latest, use PUID/PGID/TZ, and explain that SSDP needs host networking. - .dockerignore excludes the npm toolchain (bundle stays in html/js). Docs: README rewritten for the fork (about, registry, compose, env vars, security notes); README-DEV gains a container section.
512 lines
10 KiB
Go
Executable File
512 lines
10 KiB
Go
Executable File
package authentication
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
|
|
"time"
|
|
//"fmt"
|
|
//"log"
|
|
)
|
|
|
|
const tokenLength = 40
|
|
const saltLength = 20
|
|
const idLength = 10
|
|
|
|
var tokenValidity int
|
|
var database string
|
|
|
|
var databaseFile = "authentication.json"
|
|
|
|
var data = make(map[string]any)
|
|
var tokens = make(map[string]any)
|
|
var tokensMu sync.RWMutex
|
|
|
|
var initAuthentication = false
|
|
|
|
// Cookie : cookie
|
|
type Cookie struct {
|
|
Name string
|
|
Value string
|
|
Path string
|
|
Domain string
|
|
Expires time.Time
|
|
RawExpires string
|
|
}
|
|
|
|
// Init : databasePath = Path to authentication.json
|
|
func Init(databasePath string, validity int) (err error) {
|
|
database = filepath.Dir(databasePath) + string(os.PathSeparator) + databaseFile
|
|
|
|
// Check if the database already exists
|
|
if _, err = os.Stat(database); os.IsNotExist(err) {
|
|
// Create an empty database
|
|
var defaults = make(map[string]any)
|
|
defaults["dbVersion"] = "1.0"
|
|
defaults["hash"] = "sha256"
|
|
defaults["users"] = make(map[string]any)
|
|
|
|
if saveDatabase(defaults) != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Loading the database
|
|
err = loadDatabase()
|
|
|
|
// Set Token Validity
|
|
tokenValidity = validity
|
|
initAuthentication = true
|
|
return
|
|
}
|
|
|
|
// CreateDefaultUser = created efault user
|
|
func CreateDefaultUser(username, password string) (err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var users = data["users"].(map[string]any)
|
|
// Check if the default user exists
|
|
if len(users) > 0 {
|
|
err = createError(001)
|
|
return
|
|
}
|
|
|
|
var defaults = defaultsForNewUser(username, password)
|
|
users[defaults["_id"].(string)] = defaults
|
|
saveDatabase(data)
|
|
|
|
return
|
|
}
|
|
|
|
// CreateNewUser : create new user
|
|
func CreateNewUser(username, password string) (userID string, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var checkIfTheUserAlreadyExists = func(username string, userData map[string]any) (err error) {
|
|
var salt = userData["_salt"].(string)
|
|
var loginUsername = userData["_username"].(string)
|
|
|
|
if SHA256(username, salt) == loginUsername {
|
|
err = createError(020)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
var users = data["users"].(map[string]any)
|
|
for _, userData := range users {
|
|
err = checkIfTheUserAlreadyExists(username, userData.(map[string]any))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
var defaults = defaultsForNewUser(username, password)
|
|
userID = defaults["_id"].(string)
|
|
users[userID] = defaults
|
|
|
|
saveDatabase(data)
|
|
|
|
return
|
|
}
|
|
|
|
// UserAuthentication : user authentication
|
|
func UserAuthentication(username, password string) (token string, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var login = func(username, password string, loginData map[string]any) (err error) {
|
|
err = createError(010)
|
|
|
|
var salt = loginData["_salt"].(string)
|
|
var loginUsername = loginData["_username"].(string)
|
|
var loginPassword = loginData["_password"].(string)
|
|
|
|
if SHA256(username, salt) == loginUsername {
|
|
if SHA256(password, salt) == loginPassword {
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
var users = data["users"].(map[string]any)
|
|
for id, loginData := range users {
|
|
err = login(username, password, loginData.(map[string]any))
|
|
if err == nil {
|
|
token = setToken(id, "-")
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// CheckTheValidityOfTheToken : check token
|
|
func CheckTheValidityOfTheToken(token string) (newToken string, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(011)
|
|
|
|
tokensMu.Lock()
|
|
defer tokensMu.Unlock()
|
|
|
|
if v, ok := tokens[token]; ok {
|
|
expires := v.(map[string]any)["expires"].(time.Time)
|
|
|
|
if expires.Sub(time.Now().Local()) < 0 {
|
|
delete(tokens, token)
|
|
return
|
|
}
|
|
|
|
// Keep a stable token per session and only refresh expiration.
|
|
v.(map[string]any)["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
|
|
newToken = token
|
|
err = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetUserID : get user ID
|
|
func GetUserID(token string) (userID string, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(002)
|
|
|
|
tokensMu.Lock()
|
|
defer tokensMu.Unlock()
|
|
|
|
if v, ok := tokens[token]; ok {
|
|
expires := v.(map[string]any)["expires"].(time.Time)
|
|
userID = v.(map[string]any)["id"].(string)
|
|
|
|
if expires.Sub(time.Now().Local()) < 0 {
|
|
delete(tokens, token)
|
|
return
|
|
}
|
|
|
|
err = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// WriteUserData : save user date
|
|
func WriteUserData(userID string, userData map[string]any) (err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(030)
|
|
|
|
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
|
|
|
|
v["data"] = userData
|
|
err = saveDatabase(data)
|
|
|
|
} else {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ReadUserData : load user date
|
|
func ReadUserData(userID string) (userData map[string]any, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(031)
|
|
|
|
if v, ok := data["users"].(map[string]any)[userID].(map[string]any); ok {
|
|
userData = v["data"].(map[string]any)
|
|
err = nil
|
|
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// RemoveUser : remove user
|
|
func RemoveUser(userID string) (err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(032)
|
|
|
|
if _, ok := data["users"].(map[string]any)[userID]; ok {
|
|
|
|
delete(data["users"].(map[string]any), userID)
|
|
err = saveDatabase(data)
|
|
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// SetDefaultUserData : set default user data
|
|
func SetDefaultUserData(defaults map[string]any) (err error) {
|
|
|
|
allUserData, err := GetAllUserData()
|
|
|
|
for _, d := range allUserData {
|
|
var data = d.(map[string]any)["data"].(map[string]any)
|
|
var userID = d.(map[string]any)["_id"].(string)
|
|
|
|
for k, v := range defaults {
|
|
if _, ok := data[k]; ok {
|
|
// Key exist
|
|
} else {
|
|
data[k] = v
|
|
}
|
|
}
|
|
err = WriteUserData(userID, data)
|
|
}
|
|
return
|
|
}
|
|
|
|
// ChangeCredentials : change credentials
|
|
func ChangeCredentials(userID, username, password string) (err error) {
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = createError(032)
|
|
|
|
if userData, ok := data["users"].(map[string]any)[userID]; ok {
|
|
//var userData = tmp.(map[string]interface{})
|
|
var salt = userData.(map[string]any)["_salt"].(string)
|
|
|
|
if len(username) > 0 {
|
|
userData.(map[string]any)["_username"] = SHA256(username, salt)
|
|
}
|
|
|
|
if len(password) > 0 {
|
|
userData.(map[string]any)["_password"] = SHA256(password, salt)
|
|
}
|
|
|
|
err = saveDatabase(data)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GetAllUserData : get all user data
|
|
func GetAllUserData() (allUserData map[string]any, err error) {
|
|
|
|
err = checkInit()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(data) == 0 {
|
|
var defaults = make(map[string]any)
|
|
defaults["dbVersion"] = "1.0"
|
|
defaults["hash"] = "sha256"
|
|
defaults["users"] = make(map[string]any)
|
|
saveDatabase(defaults)
|
|
data = defaults
|
|
}
|
|
|
|
allUserData = data["users"].(map[string]any)
|
|
return
|
|
}
|
|
|
|
// CheckTheValidityOfTheTokenFromHTTPHeader : get token from HTTP header
|
|
func CheckTheValidityOfTheTokenFromHTTPHeader(w http.ResponseWriter, r *http.Request) (writer http.ResponseWriter, newToken string, err error) {
|
|
err = createError(011)
|
|
for _, cookie := range r.Cookies() {
|
|
if cookie.Name == "Token" {
|
|
var token string
|
|
token, err = CheckTheValidityOfTheToken(cookie.Value)
|
|
//fmt.Println("T", token, err)
|
|
writer = SetCookieToken(w, token)
|
|
newToken = token
|
|
}
|
|
}
|
|
//fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
// Framework tools
|
|
|
|
func checkInit() (err error) {
|
|
if !initAuthentication {
|
|
err = createError(000)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func saveDatabase(tmpMap any) (err error) {
|
|
|
|
jsonString, err := json.MarshalIndent(tmpMap, "", " ")
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = os.WriteFile(database, []byte(jsonString), 0600)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func loadDatabase() (err error) {
|
|
jsonString, err := os.ReadFile(database)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(jsonString), &data)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// SHA256 : password + salt = sha256 string
|
|
func SHA256(secret, salt string) string {
|
|
key := []byte(secret)
|
|
h := hmac.New(sha256.New, key)
|
|
h.Write([]byte("_remote_db"))
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func randomString(n int) string {
|
|
const alphanum = "-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789aBcDeFgHiJkLmNoPqRsTuVwXyZ_"
|
|
|
|
var bytes = make([]byte, n)
|
|
rand.Read(bytes)
|
|
for i, b := range bytes {
|
|
bytes[i] = alphanum[b%byte(len(alphanum))]
|
|
}
|
|
return string(bytes)
|
|
}
|
|
|
|
func randomID(n int) string {
|
|
const alphanum = "ABCDEFGHJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
var bytes = make([]byte, n)
|
|
rand.Read(bytes)
|
|
for i, b := range bytes {
|
|
bytes[i] = alphanum[b%byte(len(alphanum))]
|
|
}
|
|
return string(bytes)
|
|
}
|
|
|
|
func createError(errCode int) (err error) {
|
|
var errMsg string
|
|
switch errCode {
|
|
case 000:
|
|
errMsg = "Authentication has not yet been initialized"
|
|
case 001:
|
|
errMsg = "Default user already exists"
|
|
case 002:
|
|
errMsg = "No user id found for this token"
|
|
case 010:
|
|
errMsg = "User authentication failed"
|
|
case 011:
|
|
errMsg = "Session has expired"
|
|
case 020:
|
|
errMsg = "User already exists"
|
|
case 030:
|
|
errMsg = "User data could not be saved"
|
|
case 031:
|
|
errMsg = "User data could not be read"
|
|
case 032:
|
|
errMsg = "User ID was not found"
|
|
}
|
|
|
|
err = errors.New(errMsg)
|
|
return
|
|
}
|
|
|
|
func defaultsForNewUser(username, password string) map[string]any {
|
|
var defaults = make(map[string]any)
|
|
var salt = randomString(saltLength)
|
|
defaults["_username"] = SHA256(username, salt)
|
|
defaults["_password"] = SHA256(password, salt)
|
|
defaults["_salt"] = salt
|
|
defaults["_id"] = "id-" + randomID(idLength)
|
|
//defaults["_one.time.token"] = randomString(tokenLength)
|
|
defaults["data"] = make(map[string]any)
|
|
|
|
return defaults
|
|
}
|
|
|
|
func setToken(id, oldToken string) (newToken string) {
|
|
tokensMu.Lock()
|
|
defer tokensMu.Unlock()
|
|
|
|
if oldToken != "-" {
|
|
delete(tokens, oldToken)
|
|
}
|
|
|
|
loopToken:
|
|
newToken = randomString(tokenLength)
|
|
if _, ok := tokens[newToken]; ok {
|
|
goto loopToken
|
|
}
|
|
|
|
var tmp = make(map[string]any)
|
|
tmp["id"] = id
|
|
tmp["expires"] = time.Now().Local().Add(time.Minute * time.Duration(tokenValidity))
|
|
|
|
tokens[newToken] = tmp
|
|
|
|
return
|
|
}
|
|
|
|
// SetCookieToken : set cookie
|
|
func SetCookieToken(w http.ResponseWriter, token string) http.ResponseWriter {
|
|
expiration := time.Now().Add(time.Minute * time.Duration(tokenValidity))
|
|
cookie := http.Cookie{Name: "Token", Value: token, Expires: expiration}
|
|
http.SetCookie(w, &cookie)
|
|
return w
|
|
}
|