diff --git a/api.go b/api.go index 198a4b8..51b7d6c 100644 --- a/api.go +++ b/api.go @@ -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")) +} diff --git a/data/static/setup.js b/data/static/setup.js index 266979d..d15f35e 100644 --- a/data/static/setup.js +++ b/data/static/setup.js @@ -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'); diff --git a/data/currently-unused-templates/setup.html b/data/templates/setup.html similarity index 100% rename from data/currently-unused-templates/setup.html rename to data/templates/setup.html diff --git a/jfa-go b/jfa-go index 3c04f2b..1920e88 100755 Binary files a/jfa-go and b/jfa-go differ diff --git a/jfapi.go b/jfapi.go index 45f5e5a..e45e17d 100644 --- a/jfapi.go +++ b/jfapi.go @@ -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 diff --git a/main.go b/main.go index da54e5d..f098199 100644 --- a/main.go +++ b/main.go @@ -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) } diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..2fc0dcd --- /dev/null +++ b/setup.go @@ -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}) +}