381 lines
9.7 KiB
Go
381 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// IP address to use when connecting to local containers.
|
|
var containerAddress string
|
|
|
|
// Path to compiled plugin DLL to install in local containers.
|
|
var pluginPath string
|
|
|
|
// Randomly generated password used to setup container with.
|
|
var containerPassword string
|
|
|
|
func flags() {
|
|
flag.StringVar(&pluginPath, "dll", "", "Path to plugin DLL to install in container images.")
|
|
flag.StringVar(&containerAddress, "caddr", "", "IP address to use when connecting to local containers.")
|
|
flag.Parse()
|
|
|
|
// Randomize the container's password
|
|
rawPassword := make([]byte, 32)
|
|
if _, err := rand.Read(rawPassword); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
containerPassword = hex.EncodeToString(rawPassword)
|
|
}
|
|
|
|
func main() {
|
|
flags()
|
|
|
|
start := time.Now()
|
|
|
|
fmt.Printf("[+] Start time: %s\n", start)
|
|
|
|
// Load list of servers
|
|
fmt.Println("[+] Loading configuration")
|
|
config := loadConfiguration()
|
|
fmt.Println()
|
|
|
|
// Start Selenium by bringing up the compose file in detatched mode
|
|
fmt.Println("[+] Starting Selenium")
|
|
RunProgram("docker-compose", []string{"up", "-d"}, 10*time.Second)
|
|
|
|
// If any error occurs, bring Selenium down before exiting
|
|
defer func() {
|
|
fmt.Println("[+] Stopping Selenium")
|
|
RunProgram("docker-compose", []string{"down"}, 15*time.Second)
|
|
}()
|
|
|
|
// Test all provided Jellyfin servers
|
|
for _, server := range config.Servers {
|
|
if server.Skip {
|
|
continue
|
|
}
|
|
|
|
var configurationDirectory string
|
|
var apiKey string
|
|
var seleniumArgs []string
|
|
|
|
// LSIO containers use some slighly different paths & permissions
|
|
lsioImage := strings.Contains(server.Image, "linuxserver")
|
|
|
|
fmt.Println()
|
|
fmt.Printf("[+] Testing %s\n", server.Comment)
|
|
|
|
if server.Docker {
|
|
var err error
|
|
|
|
// Setup a temporary folder for the container's configuration
|
|
configurationDirectory, err = os.MkdirTemp("/dev/shm", "jf-e2e-*")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Create a folder to install the plugin into
|
|
pluginDirectory := path.Join(configurationDirectory, "plugins", "intro-skipper")
|
|
if lsioImage {
|
|
pluginDirectory = path.Join(configurationDirectory, "data", "plugins", "intro-skipper")
|
|
}
|
|
|
|
fmt.Println(" [+] Creating plugin directory")
|
|
if err := os.MkdirAll(pluginDirectory, 0700); err != nil {
|
|
fmt.Printf(" [!] Failed to create plugin directory: %s\n", err)
|
|
goto cleanup
|
|
}
|
|
|
|
// If this is an LSIO container, adjust the permissions on the plugin directory
|
|
if lsioImage {
|
|
RunProgram(
|
|
"chown",
|
|
[]string{
|
|
"911:911",
|
|
"-R",
|
|
path.Join(configurationDirectory, "data", "plugins")},
|
|
2*time.Second)
|
|
}
|
|
|
|
// Install the plugin
|
|
fmt.Printf(" [+] Copying plugin %s to %s\n", pluginPath, pluginDirectory)
|
|
RunProgram("cp", []string{pluginPath, pluginDirectory}, 2*time.Second)
|
|
fmt.Println()
|
|
|
|
/* Start the container with the following settings:
|
|
* Name: jf-e2e
|
|
* Port: 8097
|
|
* Media: Mounted to /media, read only
|
|
*/
|
|
containerArgs := []string{"run", "--name", "jf-e2e", "--rm", "-p", "8097:8096",
|
|
"-v", fmt.Sprintf("%s:%s:rw", configurationDirectory, "/config"),
|
|
"-v", fmt.Sprintf("%s:%s:ro", config.Common.Library, "/media"),
|
|
server.Image}
|
|
|
|
fmt.Printf(" [+] Starting container %s\n", server.Image)
|
|
go RunProgram("docker", containerArgs, 60*time.Second)
|
|
|
|
// Wait for the container to fully start
|
|
waitForServerStartup(server.Address)
|
|
fmt.Println()
|
|
|
|
fmt.Println(" [+] Setting up container")
|
|
|
|
// Set up the container
|
|
SetupServer(server.Address, containerPassword)
|
|
|
|
// Restart the container and wait for it to come back up
|
|
RunProgram("docker", []string{"restart", "jf-e2e"}, 10*time.Second)
|
|
time.Sleep(time.Second)
|
|
waitForServerStartup(server.Address)
|
|
fmt.Println()
|
|
} else {
|
|
fmt.Println("[+] Remote instance, assuming plugin is already installed")
|
|
}
|
|
|
|
// Get an API key
|
|
apiKey = login(server)
|
|
|
|
// Rescan the library if this is a server that we just setup
|
|
if server.Docker {
|
|
fmt.Println(" [+] Rescanning library")
|
|
|
|
sendRequest(
|
|
server.Address+"/ScheduledTasks/Running/7738148ffcd07979c7ceb148e06b3aed?api_key="+apiKey,
|
|
"POST",
|
|
"")
|
|
|
|
// TODO: poll for task completion
|
|
time.Sleep(10 * time.Second)
|
|
|
|
fmt.Println()
|
|
}
|
|
|
|
// Analyze episodes and save report
|
|
fmt.Println(" [+] Analyzing episodes")
|
|
fmt.Print("\033[37;1m") // change the color of the verifier's text
|
|
RunProgram(
|
|
"./verifier/verifier",
|
|
[]string{
|
|
"-address", server.Address,
|
|
"-key", apiKey, "-o",
|
|
fmt.Sprintf("reports/%s-%d.json", server.Comment, start.Unix())},
|
|
5*time.Minute)
|
|
fmt.Print("\033[39;0m") // reset terminal text color
|
|
|
|
// Pause for any manual tests
|
|
if server.ManualTests {
|
|
fmt.Println(" [!] Pausing for manual tests")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
reader.ReadString('\n')
|
|
}
|
|
|
|
// Setup base Selenium arguments
|
|
seleniumArgs = []string{
|
|
"-u", // force stdout to be unbuffered
|
|
"main.py",
|
|
"-host", server.Address,
|
|
"-user", server.Username,
|
|
"-pass", server.Password,
|
|
"-name", config.Common.Episode}
|
|
|
|
// Append all requested Selenium tests
|
|
seleniumArgs = append(seleniumArgs, "--tests")
|
|
seleniumArgs = append(seleniumArgs, server.Tests...)
|
|
|
|
// Append all requested browsers
|
|
seleniumArgs = append(seleniumArgs, "--browsers")
|
|
seleniumArgs = append(seleniumArgs, server.Browsers...)
|
|
|
|
// Run Selenium
|
|
os.Chdir("selenium")
|
|
RunProgram("python3", seleniumArgs, time.Minute)
|
|
os.Chdir("..")
|
|
|
|
cleanup:
|
|
if server.Docker {
|
|
// Stop the container
|
|
fmt.Println(" [+] Stopping and removing container")
|
|
RunProgram("docker", []string{"stop", "jf-e2e"}, 10*time.Second)
|
|
|
|
// Cleanup the container's configuration
|
|
fmt.Printf(" [+] Deleting %s\n", configurationDirectory)
|
|
|
|
if err := os.RemoveAll(configurationDirectory); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Login to the specified Jellyfin server and return an API key
|
|
func login(server Server) string {
|
|
type AuthenticateUserByName struct {
|
|
AccessToken string
|
|
}
|
|
|
|
fmt.Println(" [+] Sending authentication request")
|
|
|
|
// Create request body
|
|
rawBody := fmt.Sprintf(`{"Username":"%s","Pw":"%s"}`, server.Username, server.Password)
|
|
body := bytes.NewBufferString(rawBody)
|
|
|
|
// Create the request
|
|
req, err := http.NewRequest(
|
|
"POST",
|
|
fmt.Sprintf("%s/Users/AuthenticateByName", server.Address),
|
|
body)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(
|
|
"X-Emby-Authorization",
|
|
`MediaBrowser Client="JF E2E Tests", Version="0.0.1", DeviceId="E2E", Device="E2E"`)
|
|
|
|
// Authenticate
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
panic(err)
|
|
} else if res.StatusCode != http.StatusOK {
|
|
panic(fmt.Sprintf("authentication returned code %d", res.StatusCode))
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
// Read body
|
|
fullBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Unmarshal body and return token
|
|
var token AuthenticateUserByName
|
|
if err := json.Unmarshal(fullBody, &token); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return token.AccessToken
|
|
}
|
|
|
|
// Wait up to ten seconds for the provided Jellyfin server to fully startup
|
|
func waitForServerStartup(address string) {
|
|
attempts := 10
|
|
fmt.Println(" [+] Waiting for server to finish starting")
|
|
|
|
for {
|
|
// Sleep in between requests
|
|
time.Sleep(time.Second)
|
|
|
|
// Ping the /System/Info/Public endpoint
|
|
res, err := http.Get(fmt.Sprintf("%s/System/Info/Public", address))
|
|
|
|
// If the server didn't return 200 OK, loop
|
|
if err != nil || res.StatusCode != http.StatusOK {
|
|
if attempts--; attempts <= 0 {
|
|
panic("server is taking too long to startup")
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// Assume startup has finished, break
|
|
break
|
|
}
|
|
}
|
|
|
|
// Read configuration from config.json
|
|
func loadConfiguration() Configuration {
|
|
var config Configuration
|
|
|
|
// Load the contents of the configuration file
|
|
raw, err := os.ReadFile("config.json")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Unmarshal
|
|
if err := json.Unmarshal(raw, &config); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Print debugging info
|
|
fmt.Printf("Library: %s\n", config.Common.Library)
|
|
fmt.Printf("Episode: \"%s\"\n", config.Common.Episode)
|
|
fmt.Printf("Password: %s\n", containerPassword)
|
|
fmt.Println()
|
|
|
|
// Check the validity of all entries
|
|
for i, server := range config.Servers {
|
|
// If this is an entry for a local container, ensure the server address is correct
|
|
if server.Image != "" {
|
|
// Ensure that values were provided for the host's IP address, base configuration directory,
|
|
// and a path to the compiled plugin DLL to install.
|
|
if containerAddress == "" {
|
|
panic("The -caddr argument is required.")
|
|
}
|
|
|
|
if pluginPath == "" {
|
|
panic("The -dll argument is required.")
|
|
}
|
|
|
|
server.Username = "admin"
|
|
server.Password = containerPassword
|
|
server.Address = fmt.Sprintf("http://%s:8097", containerAddress)
|
|
server.Docker = true
|
|
}
|
|
|
|
// If no browsers were specified, default to Chrome (for speed)
|
|
if len(server.Browsers) == 0 {
|
|
server.Browsers = []string{"chrome"}
|
|
}
|
|
|
|
// If no tests were specified, only test that the plugin settings page works
|
|
if len(server.Tests) == 0 {
|
|
server.Tests = []string{"settings"}
|
|
}
|
|
|
|
// Verify that an address was provided
|
|
if len(server.Address) == 0 {
|
|
panic("Server address is required")
|
|
}
|
|
|
|
fmt.Printf("===== Server: %s =====\n", server.Comment)
|
|
|
|
if server.Skip {
|
|
fmt.Println("Skip: true")
|
|
}
|
|
|
|
fmt.Printf("Docker: %t\n", server.Docker)
|
|
if server.Docker {
|
|
fmt.Printf("Image: %s\n", server.Image)
|
|
}
|
|
|
|
fmt.Printf("Address: %s\n", server.Address)
|
|
fmt.Printf("Browsers: %v\n", server.Browsers)
|
|
fmt.Printf("Tests: %v\n", server.Tests)
|
|
fmt.Println()
|
|
|
|
config.Servers[i] = server
|
|
}
|
|
|
|
fmt.Println("=================")
|
|
|
|
return config
|
|
}
|