Added setup, self restarts

This commit is contained in:
Harvey Tindall 2020-08-02 00:05:35 +01:00
parent 62621dabb9
commit f508b65fa0
7 changed files with 254 additions and 117 deletions

58
api.go
View File

@ -6,6 +6,9 @@ import (
"github.com/knz/strtime"
"github.com/lithammer/shortuuid/v3"
"gopkg.in/ini.v1"
"os"
//"os/exec"
"syscall"
"time"
)
@ -572,5 +575,60 @@ func (ctx *appContext) ModifyConfig(gc *gin.Context) {
tempConfig.SaveTo(ctx.config_path)
ctx.debug.Println("Config saved")
gc.JSON(200, map[string]bool{"success": true})
if req["restart-program"].(bool) {
ctx.info.Println("Restarting...")
err := Restart()
if err != nil {
ctx.err.Printf("Couldn't restart, try restarting manually. (%s)", err)
}
}
ctx.loadConfig()
}
// func Restart() error {
// defer func() {
// if r := recover(); r != nil {
// os.Exit(0)
// }
// }()
// cwd, err := os.Getwd()
// if err != nil {
// return err
// }
// args := os.Args
// // for _, key := range args {
// // fmt.Println(key)
// // }
// cmd := exec.Command(args[0], args[1:]...)
// cmd.Stdout = os.Stdout
// cmd.Stderr = os.Stderr
// cmd.Dir = cwd
// err = cmd.Start()
// if err != nil {
// return err
// }
// // cmd.Process.Release()
// panic(fmt.Errorf("restarting"))
// }
func Restart() error {
defer func() {
if r := recover(); r != nil {
os.Exit(0)
}
}()
args := os.Args
// After a single restart, args[0] gets messed up and isnt the real executable.
// JFA_DEEP tells the new process its a child, and JFA_EXEC is the real executable
if os.Getenv("JFA_DEEP") == "" {
os.Setenv("JFA_DEEP", "1")
os.Setenv("JFA_EXEC", args[0])
}
env := os.Environ()
fmt.Printf("EXECUTABLE: %s\n", os.Getenv("JFA_EXEC"))
err := syscall.Exec(os.Getenv("JFA_EXEC"), []string{""}, env)
if err != nil {
return err
}
panic(fmt.Errorf("restarting"))
}

View File

@ -21,16 +21,19 @@ function checkEmailRadio() {
document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = '';
document.getElementById('emailMailgunArea').style.display = 'none';
document.getElementById('notificationsEnabled').checked = true;
} else if (document.getElementById('emailMailgunRadio').checked) {
document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = '';
document.getElementById('notificationsEnabled').checked = true;
} else if (document.getElementById('emailDisabledRadio').checked) {
document.getElementById('emailCommonArea').style.display = 'none';
document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = 'none';
document.getElementById('emailNextButton').href = '#page-8';
document.getElementById('valBackButton').href = '#page-4';
document.getElementById('notificationsEnabled').checked = false;
};
};
var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio'];
@ -165,6 +168,7 @@ document.getElementById('submitButton').onclick = function() {
if (document.getElementById('emailDisabledRadio').checked) {
config['password_resets']['enabled'] = 'false';
config['invite_emails']['enabled'] = 'false';
config['notifications']['enabled'] = 'false';
} else {
if (document.getElementById('emailSMTPRadio').checked) {
if (document.getElementById('emailSSL_TLS').checked) {
@ -226,6 +230,7 @@ document.getElementById('submitButton').onclick = function() {
config['ui']['help_message'] = document.getElementById('msgHelp').value;
config['ui']['success_message'] = document.getElementById('msgSuccess').value;
// Send it
config["restart-program"] = true;
var req = new XMLHttpRequest();
req.open("POST", "/modifyConfig", true);
req.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');

BIN
jfa-go

Binary file not shown.

View File

@ -82,10 +82,18 @@ func (jf *Jellyfin) authenticate(username, password string) (map[string]interfac
jf.loginParams = map[string]string{
"Username": username,
"Pw": password,
"Password": password,
}
loginParams, _ := json.Marshal(jf.loginParams)
url := fmt.Sprintf("%s/emby/Users/AuthenticateByName", jf.server)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(loginParams))
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(jf.loginParams)
if err != nil {
return nil, 0, err
}
// loginParams, _ := json.Marshal(jf.loginParams)
url := fmt.Sprintf("%s/Users/authenticatebyname", jf.server)
req, err := http.NewRequest("POST", url, buffer)
defer jf.timeoutHandler()
if err != nil {
return nil, 0, err

269
main.go
View File

@ -46,6 +46,7 @@ type appContext struct {
info, debug, err *log.Logger
host string
port int
version string
}
func GenerateSecret(length int) (string, error) {
@ -103,7 +104,7 @@ func main() {
port := flag.Int("port", 0, "alternate port to host web ui on.")
flag.Parse()
fmt.Println(*dataPath, *configPath, *host, *port)
if ctx.config_path == *configPath && ctx.data_path != *dataPath {
ctx.config_path = filepath.Join(*dataPath, "config.ini")
} else {
@ -111,12 +112,24 @@ func main() {
ctx.data_path = *dataPath
}
//var firstRun bool
// Env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
ctx.config_path = v
}
if v := os.Getenv("JFA_DATAPATH"); v != "" {
ctx.data_path = v
}
os.Setenv("JFA_CONFIGPATH", ctx.config_path)
os.Setenv("JFA_DATAPATH", ctx.data_path)
var firstRun bool
if _, err := os.Stat(ctx.data_path); os.IsNotExist(err) {
os.Mkdir(ctx.data_path, 0700)
}
if _, err := os.Stat(ctx.config_path); os.IsNotExist(err) {
//firstRun = true
firstRun = true
dConfigPath := filepath.Join(ctx.local_path, "config-default.ini")
var dConfig *os.File
dConfig, err = os.Open(dConfigPath)
@ -136,116 +149,135 @@ func main() {
}
ctx.info.Printf("Copied default configuration to \"%s\"", ctx.config_path)
}
var debugMode bool
var address string
if ctx.loadConfig() != nil {
ctx.err.Fatalf("Failed to load config file \"%s\"", ctx.config_path)
}
ctx.version = ctx.config.Section("jellyfin").Key("version").String()
ctx.host = ctx.config.Section("ui").Key("host").String()
ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056)
if *host != ctx.host && *host != "" {
ctx.host = *host
}
if *port != ctx.port && *port > 0 {
ctx.port = *port
}
address := fmt.Sprintf("%s:%d", ctx.host, ctx.port)
debugMode := ctx.config.Section("ui").Key("debug").MustBool(true)
debugMode = ctx.config.Section("ui").Key("debug").MustBool(true)
if debugMode {
ctx.debug = log.New(os.Stdout, "[DEBUG] ", log.Ltime|log.Lshortfile)
} else {
ctx.debug = log.New(ioutil.Discard, "", 0)
}
ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path)
if !firstRun {
ctx.host = ctx.config.Section("ui").Key("host").String()
ctx.port = ctx.config.Section("ui").Key("port").MustInt(8056)
if ctx.config.Section("ui").Key("bs5").MustBool(false) {
ctx.cssFile = "bs5-jf.css"
ctx.bsVersion = 5
} else {
ctx.cssFile = "bs4-jf.css"
ctx.bsVersion = 4
}
ctx.debug.Println("Loading storage")
ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json")
ctx.storage.loadInvites()
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json")
ctx.storage.loadEmails()
ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json")
ctx.storage.loadPolicy()
ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json")
ctx.storage.loadConfiguration()
ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json")
ctx.storage.loadDisplayprefs()
ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json")
config_base, _ := ioutil.ReadFile(ctx.configBase_path)
json.Unmarshal(config_base, &ctx.configBase)
themes := map[string]string{
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion),
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion),
"Custom CSS": "",
}
if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok {
ctx.cssFile = val
}
ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile)
secret, err := GenerateSecret(16)
if err != nil {
ctx.err.Fatal(err)
}
os.Setenv("JFA_SECRET", secret)
ctx.jellyfinLogin = true
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val {
ctx.jellyfinLogin = false
user := User{}
user.UserID = shortuuid.New()
user.Username = ctx.config.Section("ui").Key("username").String()
user.Password = ctx.config.Section("ui").Key("password").String()
ctx.users = append(ctx.users, user)
} else {
ctx.debug.Println("Using Jellyfin for authentication")
}
server := ctx.config.Section("jellyfin").Key("server").String()
ctx.jf.init(server, "jfa-go", "0.1", "hrfee-arch", "hrfee-arch")
var status int
_, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String())
if status != 200 || err != nil {
ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
}
ctx.info.Printf("Authenticated with %s", server)
ctx.authJf.init(server, "jfa-go", "0.1", "auth", "auth")
ctx.loadStrftime()
validatorConf := ValidatorConf{
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0),
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
}
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf {
validatorConf[key] = 0
if *host != ctx.host && *host != "" {
ctx.host = *host
}
if *port != ctx.port && *port > 0 {
ctx.port = *port
}
}
ctx.validator.init(validatorConf)
ctx.email.init(ctx)
if h := os.Getenv("JFA_HOST"); h != "" {
ctx.host = h
if p := os.Getenv("JFA_PORT"); p != "" {
var port int
_, err := fmt.Sscan(p, &port)
if err == nil {
ctx.port = port
}
}
}
inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx)
go inviteDaemon.Run()
address = fmt.Sprintf("%s:%d", ctx.host, ctx.port)
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
go ctx.StartPWR()
ctx.debug.Printf("Loaded config file \"%s\"", ctx.config_path)
if ctx.config.Section("ui").Key("bs5").MustBool(false) {
ctx.cssFile = "bs5-jf.css"
ctx.bsVersion = 5
} else {
ctx.cssFile = "bs4-jf.css"
ctx.bsVersion = 4
}
ctx.debug.Println("Loading storage")
ctx.storage.invite_path = filepath.Join(ctx.data_path, "invites.json")
ctx.storage.loadInvites()
ctx.storage.emails_path = filepath.Join(ctx.data_path, "emails.json")
ctx.storage.loadEmails()
ctx.storage.policy_path = filepath.Join(ctx.data_path, "user_template.json")
ctx.storage.loadPolicy()
ctx.storage.configuration_path = filepath.Join(ctx.data_path, "user_configuration.json")
ctx.storage.loadConfiguration()
ctx.storage.displayprefs_path = filepath.Join(ctx.data_path, "user_displayprefs.json")
ctx.storage.loadDisplayprefs()
ctx.configBase_path = filepath.Join(ctx.local_path, "config-base.json")
config_base, _ := ioutil.ReadFile(ctx.configBase_path)
json.Unmarshal(config_base, &ctx.configBase)
themes := map[string]string{
"Jellyfin (Dark)": fmt.Sprintf("bs%d-jf.css", ctx.bsVersion),
"Bootstrap (Light)": fmt.Sprintf("bs%d.css", ctx.bsVersion),
"Custom CSS": "",
}
if val, ok := themes[ctx.config.Section("ui").Key("theme").String()]; ok {
ctx.cssFile = val
}
ctx.debug.Printf("Using css file \"%s\"", ctx.cssFile)
secret, err := GenerateSecret(16)
if err != nil {
ctx.err.Fatal(err)
}
os.Setenv("JFA_SECRET", secret)
ctx.jellyfinLogin = true
if val, _ := ctx.config.Section("ui").Key("jellyfin_login").Bool(); !val {
ctx.jellyfinLogin = false
user := User{}
user.UserID = shortuuid.New()
user.Username = ctx.config.Section("ui").Key("username").String()
user.Password = ctx.config.Section("ui").Key("password").String()
ctx.users = append(ctx.users, user)
} else {
ctx.debug.Println("Using Jellyfin for authentication")
}
server := ctx.config.Section("jellyfin").Key("server").String()
ctx.jf.init(server, "jfa-go", ctx.version, "hrfee-arch", "hrfee-arch")
var status int
_, status, err = ctx.jf.authenticate(ctx.config.Section("jellyfin").Key("username").String(), ctx.config.Section("jellyfin").Key("password").String())
if status != 200 || err != nil {
ctx.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
}
ctx.info.Printf("Authenticated with %s", server)
ctx.authJf.init(server, "jfa-go", ctx.version, "auth", "auth")
ctx.loadStrftime()
validatorConf := ValidatorConf{
"characters": ctx.config.Section("password_validation").Key("min_length").MustInt(0),
"uppercase characters": ctx.config.Section("password_validation").Key("upper").MustInt(0),
"lowercase characters": ctx.config.Section("password_validation").Key("lower").MustInt(0),
"numbers": ctx.config.Section("password_validation").Key("number").MustInt(0),
"special characters": ctx.config.Section("password_validation").Key("special").MustInt(0),
}
if !ctx.config.Section("password_validation").Key("enabled").MustBool(false) {
for key := range validatorConf {
validatorConf[key] = 0
}
}
ctx.validator.init(validatorConf)
ctx.email.init(ctx)
inviteDaemon := NewRepeater(time.Duration(60*time.Second), ctx)
go inviteDaemon.Run()
if ctx.config.Section("password_resets").Key("enabled").MustBool(false) {
go ctx.StartPWR()
}
} else {
debugMode = false
gin.SetMode(gin.ReleaseMode)
address = "0.0.0.0:8056"
}
ctx.info.Println("Loading routes")
@ -255,23 +287,32 @@ func main() {
router.Use(gin.Recovery())
router.Use(static.Serve("/", static.LocalFile("data/static", false)))
router.Use(static.Serve("/invite/", static.LocalFile("data/static", false)))
router.LoadHTMLGlob("data/templates/*")
router.GET("/", ctx.AdminPage)
router.GET("/getToken", ctx.GetToken)
router.POST("/newUser", ctx.NewUser)
router.GET("/invite/:invCode", ctx.InviteProxy)
router.NoRoute(ctx.NoRouteHandler)
api := router.Group("/", ctx.webAuth())
api.POST("/generateInvite", ctx.GenerateInvite)
api.GET("/getInvites", ctx.GetInvites)
api.POST("/setNotify", ctx.SetNotify)
api.POST("/deleteInvite", ctx.DeleteInvite)
api.GET("/getUsers", ctx.GetUsers)
api.POST("/modifyUsers", ctx.ModifyEmails)
api.POST("/setDefaults", ctx.SetDefaults)
api.GET("/getConfig", ctx.GetConfig)
api.POST("/modifyConfig", ctx.ModifyConfig)
ctx.info.Printf("Starting router @ %s", address)
if !firstRun {
router.GET("/", ctx.AdminPage)
router.GET("/getToken", ctx.GetToken)
router.POST("/newUser", ctx.NewUser)
router.GET("/invite/:invCode", ctx.InviteProxy)
router.Use(static.Serve("/invite/", static.LocalFile("data/static", false)))
api := router.Group("/", ctx.webAuth())
api.POST("/generateInvite", ctx.GenerateInvite)
api.GET("/getInvites", ctx.GetInvites)
api.POST("/setNotify", ctx.SetNotify)
api.POST("/deleteInvite", ctx.DeleteInvite)
api.GET("/getUsers", ctx.GetUsers)
api.POST("/modifyUsers", ctx.ModifyEmails)
api.POST("/setDefaults", ctx.SetDefaults)
api.GET("/getConfig", ctx.GetConfig)
api.POST("/modifyConfig", ctx.ModifyConfig)
ctx.info.Printf("Starting router @ %s", address)
} else {
router.GET("/", func(gc *gin.Context) {
gc.HTML(200, "setup.html", gin.H{})
})
router.POST("/testJF", ctx.TestJF)
router.POST("/modifyConfig", ctx.ModifyConfig)
ctx.info.Printf("Loading setup @ %s", address)
}
router.Run(address)
}

25
setup.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"github.com/gin-gonic/gin"
)
type testReq struct {
Host string `json:"jfHost"`
Username string `json:"jfUser"`
Password string `json:"jfPassword"`
}
func (ctx *appContext) TestJF(gc *gin.Context) {
var req testReq
gc.BindJSON(&req)
tempjf := Jellyfin{}
tempjf.init(req.Host, "jfa-go-setup", ctx.version, "auth", "auth")
_, status, err := tempjf.authenticate(req.Username, req.Password)
if !(status == 200 || status == 204) || err != nil {
ctx.info.Printf("Auth failed with code %d (%s)", status, err)
gc.JSON(401, map[string]bool{"success": false})
return
}
gc.JSON(200, map[string]bool{"success": true})
}