2020-07-29 21:11:28 +00:00
package main
import (
2020-08-05 15:58:24 +00:00
"context"
2020-07-29 21:11:28 +00:00
"crypto/rand"
"encoding/base64"
2020-07-31 15:09:30 +00:00
"encoding/json"
2020-08-01 20:20:02 +00:00
"flag"
2020-07-29 21:11:28 +00:00
"fmt"
2020-08-01 20:20:02 +00:00
"io"
2020-07-31 15:09:30 +00:00
"io/ioutil"
2020-07-31 21:07:09 +00:00
"log"
2020-09-08 22:08:50 +00:00
"net"
2020-08-05 15:58:24 +00:00
"net/http"
2020-07-29 21:11:28 +00:00
"os"
2020-09-08 22:08:50 +00:00
"os/exec"
2020-08-05 15:58:24 +00:00
"os/signal"
2020-07-29 21:11:28 +00:00
"path/filepath"
2020-09-05 20:52:23 +00:00
"runtime"
2020-11-29 18:01:10 +00:00
"strconv"
2020-09-16 10:55:35 +00:00
"strings"
2020-08-01 14:22:30 +00:00
"time"
2020-08-16 12:36:54 +00:00
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
2020-11-02 00:53:08 +00:00
"github.com/hrfee/jfa-go/common"
2020-09-24 16:51:13 +00:00
_ "github.com/hrfee/jfa-go/docs"
2020-11-02 00:53:08 +00:00
"github.com/hrfee/jfa-go/jfapi"
"github.com/hrfee/jfa-go/ombi"
2020-08-16 12:36:54 +00:00
"github.com/lithammer/shortuuid/v3"
2020-09-24 20:59:08 +00:00
"github.com/logrusorgru/aurora/v3"
2020-09-24 16:51:13 +00:00
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
2020-08-16 12:36:54 +00:00
"gopkg.in/ini.v1"
2020-07-29 21:11:28 +00:00
)
2020-11-22 16:36:43 +00:00
// User is used for auth purposes.
2020-07-29 21:11:28 +00:00
type User struct {
2020-07-31 21:07:09 +00:00
UserID string ` json:"id" `
Username string ` json:"username" `
Password string ` json:"password" `
2020-07-29 21:11:28 +00:00
}
type appContext struct {
2020-08-15 21:07:48 +00:00
// defaults *Config
2020-07-31 21:07:09 +00:00
config * ini . File
2020-11-22 16:36:43 +00:00
configPath string
configBasePath string
2020-07-31 21:07:09 +00:00
configBase map [ string ] interface { }
2020-11-22 16:36:43 +00:00
dataPath string
localPath string
2020-07-31 21:07:09 +00:00
cssFile string
bsVersion int
jellyfinLogin bool
users [ ] User
2020-08-20 19:20:31 +00:00
invalidTokens [ ] string
2020-11-02 00:53:08 +00:00
jf * jfapi . Jellyfin
authJf * jfapi . Jellyfin
ombi * ombi . Ombi
2020-07-31 21:07:09 +00:00
datePattern string
timePattern string
storage Storage
validator Validator
2020-09-13 20:07:15 +00:00
email * Emailer
2020-07-31 21:07:09 +00:00
info , debug , err * log . Logger
2020-08-01 20:20:02 +00:00
host string
port int
2020-08-01 23:05:35 +00:00
version string
2020-08-05 15:58:24 +00:00
quit chan os . Signal
2020-11-03 21:11:43 +00:00
lang Languages
2020-11-22 16:36:43 +00:00
URLBase string
2020-11-03 21:11:43 +00:00
}
2020-11-22 16:36:43 +00:00
// Languages stores the names and filenames of language files, and the index of that which is currently selected.
2020-11-03 21:11:43 +00:00
type Languages struct {
langFiles [ ] os . FileInfo // Language filenames
langOptions [ ] string // Language names
chosenIndex int
2020-07-29 21:11:28 +00:00
}
2020-10-18 20:48:20 +00:00
func ( app * appContext ) loadHTML ( router * gin . Engine ) {
customPath := app . config . Section ( "files" ) . Key ( "html_templates" ) . MustString ( "" )
2020-11-22 16:36:43 +00:00
templatePath := filepath . Join ( app . localPath , "templates" )
2020-10-18 20:48:20 +00:00
htmlFiles , err := ioutil . ReadDir ( templatePath )
if err != nil {
2020-11-22 16:36:43 +00:00
app . err . Fatalf ( "Couldn't access template directory: \"%s\"" , filepath . Join ( app . localPath , "templates" ) )
2020-10-18 20:48:20 +00:00
return
}
loadFiles := make ( [ ] string , len ( htmlFiles ) )
for i , f := range htmlFiles {
if _ , err := os . Stat ( filepath . Join ( customPath , f . Name ( ) ) ) ; os . IsNotExist ( err ) {
app . debug . Printf ( "Using default \"%s\"" , f . Name ( ) )
loadFiles [ i ] = filepath . Join ( templatePath , f . Name ( ) )
} else {
app . info . Printf ( "Using custom \"%s\"" , f . Name ( ) )
loadFiles [ i ] = filepath . Join ( filepath . Join ( customPath , f . Name ( ) ) )
}
}
router . LoadHTMLFiles ( loadFiles ... )
}
2020-11-22 16:36:43 +00:00
func generateSecret ( length int ) ( string , error ) {
2020-07-29 21:11:28 +00:00
bytes := make ( [ ] byte , length )
_ , err := rand . Read ( bytes )
if err != nil {
return "" , err
}
return base64 . URLEncoding . EncodeToString ( bytes ) , err
}
2020-08-01 13:08:55 +00:00
func setGinLogger ( router * gin . Engine , debugMode bool ) {
if debugMode {
router . Use ( gin . LoggerWithFormatter ( func ( param gin . LogFormatterParams ) string {
2020-08-01 20:20:02 +00:00
return fmt . Sprintf ( "[GIN/DEBUG] %s: %s(%s) => %d in %s; %s\n" ,
2020-08-01 13:08:55 +00:00
param . TimeStamp . Format ( "15:04:05" ) ,
param . Method ,
param . Path ,
param . StatusCode ,
param . Latency ,
2020-08-01 20:20:02 +00:00
func ( ) string {
if param . ErrorMessage != "" {
return "Error: " + param . ErrorMessage
}
return ""
} ( ) ,
2020-08-01 13:08:55 +00:00
)
} ) )
} else {
router . Use ( gin . LoggerWithFormatter ( func ( param gin . LogFormatterParams ) string {
return fmt . Sprintf ( "[GIN] %s(%s) => %d\n" ,
param . Method ,
param . Path ,
param . StatusCode ,
)
} ) )
}
}
2020-09-08 22:08:50 +00:00
var (
PLATFORM string = runtime . GOOS
SOCK string = "jfa-go.sock"
SRV * http . Server
RESTART chan bool
DATA , CONFIG , HOST * string
PORT * int
DEBUG * bool
2020-09-16 10:55:35 +00:00
TEST bool
2020-09-24 16:51:13 +00:00
SWAGGER * bool
2020-09-08 22:08:50 +00:00
)
2020-09-05 20:52:23 +00:00
2020-09-16 10:55:35 +00:00
func test ( app * appContext ) {
fmt . Printf ( "\n\n----\n\n" )
settings := map [ string ] interface { } {
2020-11-02 00:53:08 +00:00
"server" : app . jf . Server ,
"server version" : app . jf . ServerInfo . Version ,
"server name" : app . jf . ServerInfo . Name ,
"authenticated?" : app . jf . Authenticated ,
"access token" : app . jf . AccessToken ,
"username" : app . jf . Username ,
2020-09-16 10:55:35 +00:00
}
for n , v := range settings {
fmt . Println ( n , ":" , v )
}
2020-11-02 00:53:08 +00:00
users , status , err := app . jf . GetUsers ( false )
fmt . Printf ( "GetUsers: code %d err %s maplength %d\n" , status , err , len ( users ) )
2020-09-16 10:55:35 +00:00
fmt . Printf ( "View output? [y/n]: " )
var choice string
fmt . Scanln ( & choice )
if strings . Contains ( choice , "y" ) {
out , err := json . MarshalIndent ( users , "" , " " )
fmt . Print ( string ( out ) , err )
}
2020-09-16 18:19:04 +00:00
fmt . Printf ( "Enter a user to grab: " )
var username string
fmt . Scanln ( & username )
2020-11-02 00:53:08 +00:00
user , status , err := app . jf . UserByName ( username , false )
fmt . Printf ( "UserByName (%s): code %d err %s" , username , status , err )
2020-11-22 16:36:43 +00:00
out , _ := json . MarshalIndent ( user , "" , " " )
2020-09-16 18:19:04 +00:00
fmt . Print ( string ( out ) )
2020-09-16 10:55:35 +00:00
}
2020-09-08 22:08:50 +00:00
func start ( asDaemon , firstCall bool ) {
2020-08-19 13:31:41 +00:00
// app encompasses essentially all useful functions.
2020-08-16 12:36:54 +00:00
app := new ( appContext )
2020-08-19 13:31:41 +00:00
/ *
set default config , data and local paths
also , confusing naming here . data_path is not the internal ' data ' directory , rather the users . config / jfa - go folder .
local_path is the internal ' data ' directory .
* /
2020-08-01 20:20:02 +00:00
userConfigDir , _ := os . UserConfigDir ( )
2020-11-22 16:36:43 +00:00
app . dataPath = filepath . Join ( userConfigDir , "jfa-go" )
app . configPath = filepath . Join ( app . dataPath , "config.ini" )
2020-08-02 01:11:50 +00:00
executable , _ := os . Executable ( )
2020-11-22 16:36:43 +00:00
app . localPath = filepath . Join ( filepath . Dir ( executable ) , "data" )
2020-08-01 20:20:02 +00:00
2020-08-16 12:36:54 +00:00
app . info = log . New ( os . Stdout , "[INFO] " , log . Ltime )
app . err = log . New ( os . Stdout , "[ERROR] " , log . Ltime | log . Lshortfile )
2020-08-01 20:20:02 +00:00
2020-09-08 22:08:50 +00:00
if firstCall {
2020-11-22 16:36:43 +00:00
DATA = flag . String ( "data" , app . dataPath , "alternate path to data directory." )
CONFIG = flag . String ( "config" , app . configPath , "alternate path to config file." )
2020-09-08 22:08:50 +00:00
HOST = flag . String ( "host" , "" , "alternate address to host web ui on." )
PORT = flag . Int ( "port" , 0 , "alternate port to host web ui on." )
DEBUG = flag . Bool ( "debug" , false , "Enables debug logging and exposes pprof." )
2020-09-24 16:51:13 +00:00
SWAGGER = flag . Bool ( "swagger" , false , "Enable swagger at /swagger/index.html" )
2020-08-01 20:20:02 +00:00
2020-09-08 22:08:50 +00:00
flag . Parse ( )
2020-09-24 20:05:23 +00:00
if * SWAGGER {
os . Setenv ( "SWAGGER" , "1" )
}
if * DEBUG {
os . Setenv ( "DEBUG" , "1" )
}
2020-09-08 22:08:50 +00:00
}
2020-08-19 13:31:41 +00:00
2020-09-24 20:05:23 +00:00
if os . Getenv ( "SWAGGER" ) == "1" {
* SWAGGER = true
}
if os . Getenv ( "DEBUG" ) == "1" {
* DEBUG = true
}
2020-08-19 13:31:41 +00:00
// attempt to apply command line flags correctly
2020-11-22 16:36:43 +00:00
if app . configPath == * CONFIG && app . dataPath != * DATA {
app . dataPath = * DATA
app . configPath = filepath . Join ( app . dataPath , "config.ini" )
} else if app . configPath != * CONFIG && app . dataPath == * DATA {
app . configPath = * CONFIG
2020-08-01 20:20:02 +00:00
} else {
2020-11-22 16:36:43 +00:00
app . configPath = * CONFIG
app . dataPath = * DATA
2020-08-01 20:20:02 +00:00
}
2020-08-19 13:31:41 +00:00
// env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
2020-08-01 23:05:35 +00:00
if v := os . Getenv ( "JFA_CONFIGPATH" ) ; v != "" {
2020-11-22 16:36:43 +00:00
app . configPath = v
2020-08-01 23:05:35 +00:00
}
if v := os . Getenv ( "JFA_DATAPATH" ) ; v != "" {
2020-11-22 16:36:43 +00:00
app . dataPath = v
2020-08-01 23:05:35 +00:00
}
2020-11-22 16:36:43 +00:00
os . Setenv ( "JFA_CONFIGPATH" , app . configPath )
os . Setenv ( "JFA_DATAPATH" , app . dataPath )
2020-08-01 23:05:35 +00:00
var firstRun bool
2020-11-22 16:36:43 +00:00
if _ , err := os . Stat ( app . dataPath ) ; os . IsNotExist ( err ) {
os . Mkdir ( app . dataPath , 0700 )
2020-08-01 20:20:02 +00:00
}
2020-11-22 16:36:43 +00:00
if _ , err := os . Stat ( app . configPath ) ; os . IsNotExist ( err ) {
2020-08-01 23:05:35 +00:00
firstRun = true
2020-11-22 16:36:43 +00:00
dConfigPath := filepath . Join ( app . localPath , "config-default.ini" )
2020-08-01 20:20:02 +00:00
var dConfig * os . File
dConfig , err = os . Open ( dConfigPath )
if err != nil {
2020-08-16 12:36:54 +00:00
app . err . Fatalf ( "Couldn't find default config file \"%s\"" , dConfigPath )
2020-08-01 20:20:02 +00:00
}
defer dConfig . Close ( )
var nConfig * os . File
2020-11-22 16:36:43 +00:00
nConfig , err := os . Create ( app . configPath )
2020-08-01 20:20:02 +00:00
if err != nil {
2020-11-22 16:36:43 +00:00
app . err . Printf ( "Couldn't open config file for writing: \"%s\"" , app . configPath )
2020-10-20 20:16:46 +00:00
app . err . Fatalf ( "Error: %s" , err )
2020-08-01 20:20:02 +00:00
}
defer nConfig . Close ( )
_ , err = io . Copy ( nConfig , dConfig )
if err != nil {
2020-11-22 16:36:43 +00:00
app . err . Fatalf ( "Couldn't copy default config. To do this manually, copy\n%s\nto\n%s" , dConfigPath , app . configPath )
2020-08-01 20:20:02 +00:00
}
2020-11-22 16:36:43 +00:00
app . info . Printf ( "Copied default configuration to \"%s\"" , app . configPath )
2020-08-01 20:20:02 +00:00
}
2020-08-19 13:31:41 +00:00
2020-08-01 23:05:35 +00:00
var debugMode bool
var address string
2020-08-16 12:36:54 +00:00
if app . loadConfig ( ) != nil {
2020-11-22 16:36:43 +00:00
app . err . Fatalf ( "Failed to load config file \"%s\"" , app . configPath )
2020-07-31 21:07:09 +00:00
}
2020-10-30 22:51:47 +00:00
lang := app . config . Section ( "ui" ) . Key ( "language" ) . MustString ( "en-us" )
2020-11-22 16:36:43 +00:00
app . storage . lang . FormPath = filepath . Join ( app . localPath , "lang" , "form" , lang + ".json" )
2020-10-30 22:51:47 +00:00
if _ , err := os . Stat ( app . storage . lang . FormPath ) ; os . IsNotExist ( err ) {
2020-11-22 16:36:43 +00:00
app . storage . lang . FormPath = filepath . Join ( app . localPath , "lang" , "form" , "en-us.json" )
2020-10-30 22:51:47 +00:00
}
app . storage . loadLang ( )
2020-08-16 12:36:54 +00:00
app . version = app . config . Section ( "jellyfin" ) . Key ( "version" ) . String ( )
2020-08-19 13:31:41 +00:00
// read from config...
2020-08-19 13:09:48 +00:00
debugMode = app . config . Section ( "ui" ) . Key ( "debug" ) . MustBool ( false )
2020-08-19 13:31:41 +00:00
// then from flag
2020-09-08 22:08:50 +00:00
if * DEBUG {
2020-08-19 13:09:48 +00:00
debugMode = true
}
2020-08-01 13:08:55 +00:00
if debugMode {
2020-09-24 20:59:08 +00:00
app . info . Print ( aurora . Magenta ( "\n\nWARNING: Don't use debug mode in production, as it exposes pprof on the network.\n\n" ) )
2020-08-16 12:36:54 +00:00
app . debug = log . New ( os . Stdout , "[DEBUG] " , log . Ltime | log . Lshortfile )
2020-07-31 21:07:09 +00:00
} else {
2020-08-16 12:36:54 +00:00
app . debug = log . New ( ioutil . Discard , "" , 0 )
2020-07-31 21:07:09 +00:00
}
2020-09-08 22:08:50 +00:00
if asDaemon {
go func ( ) {
socket := SOCK
os . Remove ( socket )
listener , err := net . Listen ( "unix" , socket )
if err != nil {
app . err . Fatalf ( "Couldn't establish socket connection at %s\n" , SOCK )
}
c := make ( chan os . Signal , 1 )
signal . Notify ( c , os . Interrupt )
go func ( ) {
<- c
os . Remove ( socket )
os . Exit ( 1 )
} ( )
defer func ( ) {
listener . Close ( )
os . Remove ( SOCK )
} ( )
for {
con , err := listener . Accept ( )
if err != nil {
app . err . Printf ( "Couldn't read message on %s: %s" , socket , err )
continue
}
buf := make ( [ ] byte , 512 )
nr , err := con . Read ( buf )
if err != nil {
app . err . Printf ( "Couldn't read message on %s: %s" , socket , err )
continue
}
command := string ( buf [ 0 : nr ] )
if command == "stop" {
app . shutdown ( )
}
}
} ( )
}
2020-08-01 23:05:35 +00:00
if ! firstRun {
2020-08-16 12:36:54 +00:00
app . host = app . config . Section ( "ui" ) . Key ( "host" ) . String ( )
app . port = app . config . Section ( "ui" ) . Key ( "port" ) . MustInt ( 8056 )
2020-07-31 21:07:09 +00:00
2020-09-08 22:08:50 +00:00
if * HOST != app . host && * HOST != "" {
app . host = * HOST
2020-08-01 23:05:35 +00:00
}
2020-09-08 22:08:50 +00:00
if * PORT != app . port && * PORT > 0 {
app . port = * PORT
2020-08-01 23:05:35 +00:00
}
2020-08-01 20:20:02 +00:00
2020-08-01 23:05:35 +00:00
if h := os . Getenv ( "JFA_HOST" ) ; h != "" {
2020-08-16 12:36:54 +00:00
app . host = h
2020-08-01 23:05:35 +00:00
if p := os . Getenv ( "JFA_PORT" ) ; p != "" {
var port int
_ , err := fmt . Sscan ( p , & port )
if err == nil {
2020-08-16 12:36:54 +00:00
app . port = port
2020-08-01 23:05:35 +00:00
}
}
}
2020-07-31 21:07:09 +00:00
2020-08-16 12:36:54 +00:00
address = fmt . Sprintf ( "%s:%d" , app . host , app . port )
2020-07-29 21:11:28 +00:00
2020-11-22 16:36:43 +00:00
app . debug . Printf ( "Loaded config file \"%s\"" , app . configPath )
2020-07-29 21:11:28 +00:00
2020-08-16 12:36:54 +00:00
if app . config . Section ( "ui" ) . Key ( "bs5" ) . MustBool ( false ) {
app . cssFile = "bs5-jf.css"
app . bsVersion = 5
2020-08-01 23:05:35 +00:00
} else {
2020-08-16 12:36:54 +00:00
app . cssFile = "bs4-jf.css"
app . bsVersion = 4
2020-07-29 21:11:28 +00:00
}
2020-08-16 12:36:54 +00:00
app . debug . Println ( "Loading storage" )
2020-08-01 23:05:35 +00:00
2020-09-05 16:32:49 +00:00
app . storage . invite_path = app . config . Section ( "files" ) . Key ( "invites" ) . String ( )
2020-08-16 12:36:54 +00:00
app . storage . loadInvites ( )
2020-09-05 16:32:49 +00:00
app . storage . emails_path = app . config . Section ( "files" ) . Key ( "emails" ) . String ( )
2020-08-16 12:36:54 +00:00
app . storage . loadEmails ( )
2020-09-05 16:32:49 +00:00
app . storage . policy_path = app . config . Section ( "files" ) . Key ( "user_template" ) . String ( )
2020-08-16 12:36:54 +00:00
app . storage . loadPolicy ( )
2020-09-17 15:51:19 +00:00
app . storage . configuration_path = app . config . Section ( "files" ) . Key ( "user_configuration" ) . String ( )
2020-08-16 12:36:54 +00:00
app . storage . loadConfiguration ( )
2020-09-05 16:32:49 +00:00
app . storage . displayprefs_path = app . config . Section ( "files" ) . Key ( "user_displayprefs" ) . String ( )
2020-08-16 12:36:54 +00:00
app . storage . loadDisplayprefs ( )
2020-08-01 23:05:35 +00:00
2020-09-20 10:21:04 +00:00
app . storage . profiles_path = app . config . Section ( "files" ) . Key ( "user_profiles" ) . String ( )
app . storage . loadProfiles ( )
if ! ( len ( app . storage . policy ) == 0 && len ( app . storage . configuration ) == 0 && len ( app . storage . displayprefs ) == 0 ) {
app . info . Println ( "Migrating user template files to new profile format" )
app . storage . migrateToProfile ( )
2020-09-21 23:34:11 +00:00
for _ , path := range [ 3 ] string { app . storage . policy_path , app . storage . configuration_path , app . storage . displayprefs_path } {
if _ , err := os . Stat ( path ) ; ! os . IsNotExist ( err ) {
dir , fname := filepath . Split ( path )
newFname := strings . Replace ( fname , ".json" , ".old.json" , 1 )
err := os . Rename ( path , filepath . Join ( dir , newFname ) )
if err != nil {
app . err . Fatalf ( "Failed to rename %s: %s" , fname , err )
}
}
}
app . info . Println ( "In case of a problem, your original files have been renamed to <file>.old.json" )
2020-09-20 10:21:04 +00:00
app . storage . storeProfiles ( )
}
2020-09-05 16:32:49 +00:00
if app . config . Section ( "ombi" ) . Key ( "enabled" ) . MustBool ( false ) {
app . storage . ombi_path = app . config . Section ( "files" ) . Key ( "ombi_template" ) . String ( )
app . storage . loadOmbiTemplate ( )
2020-11-02 00:53:08 +00:00
ombiServer := app . config . Section ( "ombi" ) . Key ( "server" ) . String ( )
app . ombi = ombi . NewOmbi (
ombiServer ,
2020-09-05 16:32:49 +00:00
app . config . Section ( "ombi" ) . Key ( "api_key" ) . String ( ) ,
2020-11-02 00:53:08 +00:00
common . NewTimeoutHandler ( "Ombi" , ombiServer , true ) ,
2020-09-05 16:32:49 +00:00
)
2020-11-02 00:53:08 +00:00
2020-09-05 16:32:49 +00:00
}
2020-11-22 16:36:43 +00:00
app . configBasePath = filepath . Join ( app . localPath , "config-base.json" )
configBase , _ := ioutil . ReadFile ( app . configBasePath )
2020-11-02 00:53:08 +00:00
json . Unmarshal ( configBase , & app . configBase )
2020-08-01 23:05:35 +00:00
themes := map [ string ] string {
2020-08-16 12:36:54 +00:00
"Jellyfin (Dark)" : fmt . Sprintf ( "bs%d-jf.css" , app . bsVersion ) ,
"Bootstrap (Light)" : fmt . Sprintf ( "bs%d.css" , app . bsVersion ) ,
2020-08-01 23:05:35 +00:00
"Custom CSS" : "" ,
}
2020-08-16 12:36:54 +00:00
if val , ok := themes [ app . config . Section ( "ui" ) . Key ( "theme" ) . String ( ) ] ; ok {
app . cssFile = val
2020-08-01 23:05:35 +00:00
}
2020-08-16 12:36:54 +00:00
app . debug . Printf ( "Using css file \"%s\"" , app . cssFile )
2020-11-22 16:36:43 +00:00
secret , err := generateSecret ( 16 )
2020-08-01 23:05:35 +00:00
if err != nil {
2020-08-16 12:36:54 +00:00
app . err . Fatal ( err )
2020-08-01 23:05:35 +00:00
}
os . Setenv ( "JFA_SECRET" , secret )
2020-08-16 12:36:54 +00:00
app . jellyfinLogin = true
if val , _ := app . config . Section ( "ui" ) . Key ( "jellyfin_login" ) . Bool ( ) ; ! val {
app . jellyfinLogin = false
2020-08-01 23:05:35 +00:00
user := User { }
user . UserID = shortuuid . New ( )
2020-08-16 12:36:54 +00:00
user . Username = app . config . Section ( "ui" ) . Key ( "username" ) . String ( )
user . Password = app . config . Section ( "ui" ) . Key ( "password" ) . String ( )
app . users = append ( app . users , user )
2020-08-01 23:05:35 +00:00
} else {
2020-08-16 12:36:54 +00:00
app . debug . Println ( "Using Jellyfin for authentication" )
2020-08-01 23:05:35 +00:00
}
2020-07-31 11:48:37 +00:00
2020-08-16 12:36:54 +00:00
server := app . config . Section ( "jellyfin" ) . Key ( "server" ) . String ( )
2020-11-02 23:26:46 +00:00
cacheTimeout := int ( app . config . Section ( "jellyfin" ) . Key ( "cache_timeout" ) . MustUint ( 30 ) )
2020-11-02 00:53:08 +00:00
app . jf , _ = jfapi . NewJellyfin (
2020-09-29 19:51:15 +00:00
server ,
app . config . Section ( "jellyfin" ) . Key ( "client" ) . String ( ) ,
app . config . Section ( "jellyfin" ) . Key ( "version" ) . String ( ) ,
app . config . Section ( "jellyfin" ) . Key ( "device" ) . String ( ) ,
app . config . Section ( "jellyfin" ) . Key ( "device_id" ) . String ( ) ,
2020-11-02 00:53:08 +00:00
common . NewTimeoutHandler ( "Jellyfin" , server , true ) ,
2020-11-02 23:26:46 +00:00
cacheTimeout ,
2020-09-29 19:51:15 +00:00
)
2020-08-01 23:05:35 +00:00
var status int
2020-11-02 00:53:08 +00:00
_ , status , err = app . jf . Authenticate ( app . config . Section ( "jellyfin" ) . Key ( "username" ) . String ( ) , app . config . Section ( "jellyfin" ) . Key ( "password" ) . String ( ) )
2020-08-01 23:05:35 +00:00
if status != 200 || err != nil {
2020-08-16 12:36:54 +00:00
app . err . Fatalf ( "Failed to authenticate with Jellyfin @ %s: Code %d" , server , status )
2020-08-01 23:05:35 +00:00
}
2020-08-16 12:36:54 +00:00
app . info . Printf ( "Authenticated with %s" , server )
2020-11-29 18:01:10 +00:00
// from 10.7.0, jellyfin hyphenates user IDs. This checks if the version is equal or higher.
checkVersion := func ( version string ) int {
numberStrings := strings . Split ( version , "." )
n := 0
for _ , s := range numberStrings {
num , err := strconv . Atoi ( s )
if err == nil {
n += num
}
}
return n
}
if checkVersion ( app . jf . ServerInfo . Version ) >= checkVersion ( "10.7.0" ) {
noHyphens := true
for id := range app . storage . emails {
if strings . Contains ( id , "-" ) {
noHyphens = false
break
}
}
if noHyphens {
app . info . Println ( aurora . Yellow ( "From Jellyfin 10.7.0 onwards, user IDs are hyphenated.\nYour emails.json file will be modified to match this new format.\nA backup will be placed next to the file.\n" ) )
time . Sleep ( time . Second * time . Duration ( 3 ) )
newEmails , status , err := app . upgradeEmailStorage ( app . storage . emails )
if status != 200 || err != nil {
app . err . Printf ( "Failed to get users from Jellyfin: Code %d" , status )
app . debug . Printf ( "Error: %s" , err )
app . err . Fatalf ( "Couldn't upgrade emails.json" )
}
bakFile := app . storage . emails_path + ".bak"
err = storeJSON ( bakFile , app . storage . emails )
if err != nil {
app . err . Fatalf ( "couldn't store emails.json backup: %s" , err )
}
app . storage . emails = newEmails
err = app . storage . storeEmails ( )
if err != nil {
app . err . Fatalf ( "couldn't store emails.json: %s" , err )
}
}
}
2020-11-02 23:26:46 +00:00
app . authJf , _ = jfapi . NewJellyfin ( server , "jfa-go" , app . version , "auth" , "auth" , common . NewTimeoutHandler ( "Jellyfin" , server , true ) , cacheTimeout )
2020-08-01 14:22:30 +00:00
2020-08-16 12:36:54 +00:00
app . loadStrftime ( )
2020-08-01 23:05:35 +00:00
validatorConf := ValidatorConf {
2020-10-20 22:00:30 +00:00
"length" : app . config . Section ( "password_validation" ) . Key ( "min_length" ) . MustInt ( 0 ) ,
"uppercase" : app . config . Section ( "password_validation" ) . Key ( "upper" ) . MustInt ( 0 ) ,
"lowercase" : app . config . Section ( "password_validation" ) . Key ( "lower" ) . MustInt ( 0 ) ,
"number" : app . config . Section ( "password_validation" ) . Key ( "number" ) . MustInt ( 0 ) ,
"special" : app . config . Section ( "password_validation" ) . Key ( "special" ) . MustInt ( 0 ) ,
2020-08-01 23:05:35 +00:00
}
2020-08-16 12:36:54 +00:00
if ! app . config . Section ( "password_validation" ) . Key ( "enabled" ) . MustBool ( false ) {
2020-08-01 23:05:35 +00:00
for key := range validatorConf {
validatorConf [ key ] = 0
}
}
2020-08-16 12:36:54 +00:00
app . validator . init ( validatorConf )
2020-08-01 23:05:35 +00:00
2020-09-16 10:55:35 +00:00
if TEST {
test ( app )
os . Exit ( 0 )
}
2020-11-22 16:36:43 +00:00
inviteDaemon := newRepeater ( time . Duration ( 60 * time . Second ) , app )
go inviteDaemon . run ( )
2020-08-01 23:05:35 +00:00
2020-08-16 12:36:54 +00:00
if app . config . Section ( "password_resets" ) . Key ( "enabled" ) . MustBool ( false ) {
go app . StartPWR ( )
2020-08-01 23:05:35 +00:00
}
} else {
debugMode = false
address = "0.0.0.0:8056"
2020-08-01 15:31:08 +00:00
}
2020-08-16 12:36:54 +00:00
app . info . Println ( "Loading routes" )
2020-08-17 11:33:26 +00:00
if debugMode {
gin . SetMode ( gin . DebugMode )
} else {
gin . SetMode ( gin . ReleaseMode )
}
2020-08-01 13:08:55 +00:00
router := gin . New ( )
setGinLogger ( router , debugMode )
router . Use ( gin . Recovery ( ) )
2020-11-22 16:36:43 +00:00
router . Use ( static . Serve ( "/" , static . LocalFile ( filepath . Join ( app . localPath , "static" ) , false ) ) )
2020-10-18 20:48:20 +00:00
app . loadHTML ( router )
2020-08-16 12:36:54 +00:00
router . NoRoute ( app . NoRouteHandler )
2020-08-02 23:13:09 +00:00
if debugMode {
2020-08-16 12:36:54 +00:00
app . debug . Println ( "Loading pprof" )
2020-08-02 23:13:09 +00:00
pprof . Register ( router )
}
2020-08-01 23:05:35 +00:00
if ! firstRun {
2020-08-16 12:36:54 +00:00
router . GET ( "/" , app . AdminPage )
2020-11-12 21:04:35 +00:00
router . GET ( "/token/login" , app . getTokenLogin )
router . GET ( "/token/refresh" , app . getTokenRefresh )
2020-08-16 12:36:54 +00:00
router . POST ( "/newUser" , app . NewUser )
2020-11-22 16:36:43 +00:00
router . Use ( static . Serve ( "/invite/" , static . LocalFile ( filepath . Join ( app . localPath , "static" ) , false ) ) )
2020-08-16 12:36:54 +00:00
router . GET ( "/invite/:invCode" , app . InviteProxy )
2020-09-24 16:51:13 +00:00
if * SWAGGER {
2020-09-24 20:59:08 +00:00
app . info . Print ( aurora . Magenta ( "\n\nWARNING: Swagger should not be used on a public instance.\n\n" ) )
2020-09-24 16:51:13 +00:00
router . GET ( "/swagger/*any" , ginSwagger . WrapHandler ( swaggerFiles . Handler ) )
}
2020-08-16 12:36:54 +00:00
api := router . Group ( "/" , app . webAuth ( ) )
2020-08-20 19:20:31 +00:00
router . POST ( "/logout" , app . Logout )
2020-09-24 13:03:25 +00:00
api . DELETE ( "/users" , app . DeleteUser )
api . GET ( "/users" , app . GetUsers )
api . POST ( "/users" , app . NewUserAdmin )
api . POST ( "/invites" , app . GenerateInvite )
api . GET ( "/invites" , app . GetInvites )
api . DELETE ( "/invites" , app . DeleteInvite )
api . POST ( "/invites/profile" , app . SetProfile )
api . GET ( "/profiles" , app . GetProfiles )
api . POST ( "/profiles/default" , app . SetDefaultProfile )
api . POST ( "/profiles" , app . CreateProfile )
api . DELETE ( "/profiles" , app . DeleteProfile )
api . POST ( "/invites/notify" , app . SetNotify )
api . POST ( "/users/emails" , app . ModifyEmails )
2020-09-22 23:01:07 +00:00
// api.POST("/setDefaults", app.SetDefaults)
2020-09-24 13:03:25 +00:00
api . POST ( "/users/settings" , app . ApplySettings )
api . GET ( "/config" , app . GetConfig )
api . POST ( "/config" , app . ModifyConfig )
2020-09-05 16:32:49 +00:00
if app . config . Section ( "ombi" ) . Key ( "enabled" ) . MustBool ( false ) {
2020-09-24 13:03:25 +00:00
api . GET ( "/ombi/users" , app . OmbiUsers )
api . POST ( "/ombi/defaults" , app . SetOmbiDefaults )
2020-09-05 16:32:49 +00:00
}
2020-08-16 12:36:54 +00:00
app . info . Printf ( "Starting router @ %s" , address )
2020-08-01 23:05:35 +00:00
} else {
router . GET ( "/" , func ( gc * gin . Context ) {
2020-09-08 22:13:44 +00:00
gc . HTML ( 200 , "setup.html" , gin . H { } )
2020-08-01 23:05:35 +00:00
} )
2020-09-24 13:03:25 +00:00
router . POST ( "/jellyfin/test" , app . TestJF )
router . POST ( "/config" , app . ModifyConfig )
2020-08-16 12:36:54 +00:00
app . info . Printf ( "Loading setup @ %s" , address )
2020-08-01 23:05:35 +00:00
}
2020-08-15 21:07:48 +00:00
2020-09-08 22:08:50 +00:00
SRV = & http . Server {
2020-08-05 15:58:24 +00:00
Addr : address ,
Handler : router ,
}
go func ( ) {
2020-09-08 22:08:50 +00:00
if err := SRV . ListenAndServe ( ) ; err != nil {
2020-08-16 12:36:54 +00:00
app . err . Printf ( "Failure serving: %s" , err )
2020-08-05 15:58:24 +00:00
}
} ( )
2020-08-16 12:36:54 +00:00
app . quit = make ( chan os . Signal )
signal . Notify ( app . quit , os . Interrupt )
2020-09-08 22:08:50 +00:00
go func ( ) {
for range app . quit {
app . shutdown ( )
}
} ( )
for range RESTART {
cntx , cancel := context . WithTimeout ( context . Background ( ) , time . Second * 5 )
defer cancel ( )
if err := SRV . Shutdown ( cntx ) ; err != nil {
app . err . Fatalf ( "Server shutdown error: %s" , err )
}
return
}
}
func ( app * appContext ) shutdown ( ) {
2020-08-16 12:36:54 +00:00
app . info . Println ( "Shutting down..." )
2020-08-05 15:58:24 +00:00
cntx , cancel := context . WithTimeout ( context . Background ( ) , time . Second * 5 )
defer cancel ( )
2020-09-08 22:08:50 +00:00
if err := SRV . Shutdown ( cntx ) ; err != nil {
2020-08-16 12:36:54 +00:00
app . err . Fatalf ( "Server shutdown error: %s" , err )
2020-08-05 15:58:24 +00:00
}
2020-09-08 22:08:50 +00:00
os . Exit ( 1 )
}
func flagPassed ( name string ) ( found bool ) {
for _ , f := range os . Args {
if f == name {
found = true
}
}
return
}
2020-09-24 16:51:13 +00:00
// @title jfa-go internal API
// @version 0.2.0
// @description API for the jfa-go frontend
// @contact.name Harvey Tindall
// @contact.email hrfee@protonmail.ch
// @license.name MIT
// @license.url https://raw.githubusercontent.com/hrfee/jfa-go/main/LICENSE
// @BasePath /
2020-11-12 21:04:35 +00:00
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
2020-09-24 17:50:03 +00:00
// @securityDefinitions.basic getTokenAuth
// @name getTokenAuth
// @tag.name Auth
// @tag.description --------Get a token here first!--------
// @tag.name Users
// @tag.description Jellyfin user related operations.
// @tag.name Invites
// @tag.description Invite related operations.
// @tag.name Profiles & Settings
// @tag.description Profile and settings related operations.
// @tag.name Configuration
// @tag.description jfa-go settings.
// @tag.name Ombi
// @tag.description Ombi related operations.
// @tag.name Other
// @tag.description Things that dont fit elsewhere.
2020-09-24 22:57:42 +00:00
func printVersion ( ) {
2020-09-24 20:59:08 +00:00
fmt . Print ( aurora . Sprintf ( aurora . Magenta ( "jfa-go version: %s (%s)\n" ) , aurora . BrightWhite ( VERSION ) , aurora . White ( COMMIT ) ) )
2020-09-24 22:57:42 +00:00
}
func main ( ) {
printVersion ( )
2020-09-08 22:08:50 +00:00
folder := "/tmp"
if PLATFORM == "windows" {
folder = os . Getenv ( "TEMP" )
}
SOCK = filepath . Join ( folder , SOCK )
2020-09-13 20:07:15 +00:00
fmt . Println ( "Socket:" , SOCK )
2020-09-16 10:55:35 +00:00
if flagPassed ( "test" ) {
TEST = true
}
2020-09-08 22:08:50 +00:00
if flagPassed ( "start" ) {
args := [ ] string { }
for i , f := range os . Args {
if f == "start" {
args = append ( args , "daemon" )
} else if i != 0 {
args = append ( args , f )
}
}
cmd := exec . Command ( os . Args [ 0 ] , args ... )
cmd . Start ( )
os . Exit ( 1 )
} else if flagPassed ( "stop" ) {
con , err := net . Dial ( "unix" , SOCK )
if err != nil {
fmt . Printf ( "Couldn't dial socket %s, are you sure jfa-go is running?\n" , SOCK )
os . Exit ( 1 )
}
_ , err = con . Write ( [ ] byte ( "stop" ) )
if err != nil {
fmt . Printf ( "Couldn't send command to socket %s, are you sure jfa-go is running?\n" , SOCK )
os . Exit ( 1 )
}
fmt . Println ( "Sent." )
} else if flagPassed ( "daemon" ) {
start ( true , true )
} else {
RESTART = make ( chan bool , 1 )
start ( false , true )
for {
2020-09-24 22:57:42 +00:00
printVersion ( )
2020-09-08 22:08:50 +00:00
start ( false , false )
}
}
2020-07-29 21:11:28 +00:00
}