diff --git a/.env b/.env new file mode 100644 index 0000000..6641729 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SERVER_NAME=delemaco +SERVER_USER=root +MAC_ADDRESS=b8:cb:29:a1:f3:88 +PORT=8080 diff --git a/.env.sample b/.env.sample index 39aae63..ea2a15a 100644 --- a/.env.sample +++ b/.env.sample @@ -2,4 +2,3 @@ SERVER_NAME=pippo SERVER_USER=root MAC_ADDRESS=aa:aa:aa:aa:aa:aa PORT=8080 -SHUTDOWN_PASSWORD="password" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3dd69d0..a939000 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,137 +2,125 @@ name: Build and Release on: push: - branches: [main] + branches: [ main ] pull_request: - branches: [main] + branches: [ main ] jobs: build: - name: Build multi-arch Raspberry Pi binaries + name: Cross-compile for Raspberry Pi runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: "1.22" + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' - - name: Build for ARMv6 (Pi Zero / Pi 1) - run: | - GOOS=linux GOARCH=arm GOARM=6 \ - go build -ldflags="-s -w" -o wol-server-armv6 + - name: Build for ARM (Pi Zero, Pi 1 - ARMv6) + run: | + GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wol-server-arm6 - - name: Build for ARM64 (Pi Zero 2 / Pi 3 / Pi 4 / Pi 5) - run: | - GOOS=linux GOARCH=arm64 \ - go build -ldflags="-s -w" -o wol-server-arm64 + - name: Create systemd service file + run: | + cat > wol-server.service << 'EOL' + [Unit] + Description=WOL Server Go Application + After=network.target - - name: Create systemd service file - run: | - cat > wol-server.service << 'EOF' - [Unit] - Description=WOL Server Go Application - After=network.target + [Service] + User=pi + WorkingDirectory=/home/pi/wol-server + ExecStart=/home/pi/wol-server/wol-server + Restart=always - [Service] - Type=simple - User=loke - WorkingDirectory=/home/loke/wol-server - ExecStart=/home/loke/wol-server/wol-server - Restart=always - RestartSec=3 + [Install] + WantedBy=multi-user.target + EOL - [Install] - WantedBy=multi-user.target - EOF + - name: Create deployment script + run: | + cat > deploy.sh << 'EOL' + #!/bin/bash - - name: Create install script - run: | - cat > install.sh << 'EOF' - #!/bin/bash - set -e + # Detect Raspberry Pi model and use the appropriate binary + MODEL=$(cat /proc/cpuinfo | grep "Model" | sed 's/Model\s*:\s*//g') + echo "Detected model: $MODEL" - INSTALL_DIR="$HOME/wol-server" + # Select the right binary based on processor architecture + ARCH=$(uname -m) + echo "Architecture: $ARCH" - echo "Creating installation directory..." - mkdir -p "$INSTALL_DIR/templates" + if [[ "$ARCH" == "aarch64" ]]; then + echo "Using ARM64 binary" + cp wol-server-arm64 wol-server + elif [[ "$ARCH" == "armv7l" ]]; then + echo "Using ARMv7 binary" + cp wol-server-arm7 wol-server + else + echo "Using ARMv6 binary" + cp wol-server-arm6 wol-server + fi - ARCH=$(uname -m) - case "$ARCH" in - armv6l|armv7l) - BIN="wol-server-armv6" - ;; - aarch64) - BIN="wol-server-arm64" - ;; - *) - echo "Unsupported architecture: $ARCH" - exit 1 - ;; - esac + echo "Creating directory..." + mkdir -p ~/wol-server - echo "Detected architecture: $ARCH" - echo "Using binary: $BIN" + echo "Copying application..." + cp wol-server ~/wol-server/ + chmod +x ~/wol-server/wol-server - cp "$BIN" "$INSTALL_DIR/wol-server" - chmod +x "$INSTALL_DIR/wol-server" + echo "Installing service..." + sudo cp wol-server.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable wol-server + sudo systemctl restart wol-server - echo "Installing templates..." - cp -r templates/* "$INSTALL_DIR/templates/" + echo "Deployment complete!" + echo "Service status:" + sudo systemctl status wol-server + EOL - echo "Installing systemd service..." - sudo cp wol-server.service /etc/systemd/system/ - sudo systemctl daemon-reload - sudo systemctl enable wol-server + chmod +x deploy.sh - echo "Installing dependencies..." - sudo apt-get update -qq - sudo apt-get install -y wakeonlan sshpass + - name: Create archive for each platform + run: | + # Create templates directory in the package directories + mkdir -p armv6-package/templates + mkdir -p all-package/templates - echo "Starting service..." - sudo systemctl restart wol-server + # Copy template files + cp templates/* armv6-package/templates/ + cp templates/* all-package/templates/ - echo "===========================================" - echo "WOL Server installed successfully!" - echo "URL: http://$(hostname -I | awk '{print $1}'):8080" - echo "===========================================" - EOF + # ARMv6 package + cp wol-server-arm6 armv6-package/wol-server-arm6 + cp wol-server.service armv6-package/ + cp deploy.sh armv6-package/ + tar -czf wol-server-armv6.tar.gz -C armv6-package . - chmod +x install.sh + # All-in-one package + cp wol-server-arm6 all-package/ + cp wol-server.service all-package/ + cp deploy.sh all-package/ + tar -czf wol-server-all.tar.gz -C all-package . - - name: Create release package - run: | - mkdir -p package - cp wol-server-armv6 package/ - cp wol-server-arm64 package/ - cp wol-server.service package/ - cp install.sh package/ - cp -r templates package/ - - tar -czf wol-server.tar.gz -C package . - - - name: Create Forgejo Release - if: github.ref == 'refs/heads/main' - run: | - TAG="v${{ github.run_number }}" - API_URL="${{ github.server_url }}/api/v1" - REPO="${{ github.repository }}" - - # Create release - RELEASE_ID=$(curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - -H "Content-Type: application/json" \ - "${API_URL}/repos/${REPO}/releases" \ - -d "{\"tag_name\": \"${TAG}\", \"name\": \"Release ${TAG}\", \"draft\": false, \"prerelease\": false}" \ - | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) - - # Upload asset - curl -s -X POST \ - -H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \ - "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=wol-server.tar.gz" \ - -F "attachment=@wol-server.tar.gz" > /dev/null - - echo "Release ${TAG} created with asset wol-server.tar.gz" + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + if: github.ref == 'refs/heads/main' + with: + tag_name: v${{ github.run_number }} + name: Release v${{ github.run_number }} + draft: false + prerelease: false + files: | + wol-server-arm6 + wol-server.service + deploy.sh + wol-server-armv6.tar.gz + wol-server-all.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4c49bd7..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/README.md b/README.md index 0e16060..412df40 100644 --- a/README.md +++ b/README.md @@ -1,286 +1,293 @@ -# WOL Server - Wake-on-LAN Control Panel for Raspberry Pi +# WOL-Server -A lightweight web-based Wake-on-LAN control panel designed for Raspberry Pi that lets you remotely power on and shut down your network devices. - - +A lightweight Wake-on-LAN (WOL) server application designed specifically for Raspberry Pi devices. This tool allows you to remotely power on your network devices using magic packets through a simple, user-friendly interface. ## Features -- **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI -- **Status Monitoring**: Check if your target device is online with auto-refreshing UI -- **Scheduled Backup Window**: Configure automatic daily, bi-daily, weekly, or monthly server startup and shutdown for backup operations -- **Auto Shutdown**: Shut down the server automatically at the end of the backup window -- **Smart Shutdown Protection**: Only auto-shuts down servers that were started by the scheduler -- **Passwordless Operation**: Uses environment variable for all shutdown operations -- **Multiple Shutdown Methods**: Supports various SSH authentication methods for reliable automatic shutdown -- **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models -- **Secure Shutdown**: Password-protected shutdown functionality -- **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models -- **Easy Setup**: Simple installation process with clear instructions +- **Cross-Platform Compatibility**: Optimized for various Raspberry Pi models (Zero, 1, 2, 3, 4) +- **Easy Installation**: Automated deployment script for quick setup +- **Systemd Integration**: Runs as a system service for reliability +- **Simple Web Interface**: Control your devices through an intuitive web UI +- **Configurable**: Easily customize settings through environment variables or `.env` file +- **Low Resource Usage**: Minimal footprint to run efficiently on any Pi ## Installation -### Prerequisites +### Method 1: Pre-built Release (Recommended) -- Raspberry Pi (any model) running Raspberry Pi OS -- Network connection -- Basic knowledge of SSH/terminal - -### Option 1: One-Command Installation - -1. **Download the latest release** on your local machine from the [Releases page](https://github.com/thisloke/wol-server/releases) - -2. **Transfer the package to your Raspberry Pi** using SCP: +1. **Check which binary is right for your Raspberry Pi model**: ```bash - scp wol-server.tar.gz pi@your-pi-ip:~/ + uname -m ``` + - `armv6l`: Use `wol-server-arm6` (Pi Zero, Pi 1) + - `armv7l`: Use `wol-server-arm7` (Pi 2, Pi 3 32-bit) + - `aarch64`: Use `wol-server-arm64` (Pi 3/4 64-bit) + +2. **Download the latest release**: + + Navigate to [Releases](https://github.com/thisloke/wol-server/releases) and download the appropriate files for your Pi, or use the following commands: -3. **SSH into your Raspberry Pi**: ```bash - ssh pi@your-pi-ip + # Create a directory for installation + mkdir -p ~/wol-install + cd ~/wol-install + + # Download the executable (replace v6 with the latest version number) + wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server-arm6 + + # Download the service file + wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server.service + + # Download the deploy script + wget https://github.com/thisloke/wol-server/releases/download/v6/deploy.sh + + # Make files executable + chmod +x wol-server-arm6 + chmod +x deploy.sh ``` -4. **Install with a single command**: +3. **Create a `.env` file for configuration**: ```bash - tar -xzf wol-server.tar.gz && ./install.sh - ``` - -5. **Access the web interface** at: - ``` - http://your-pi-ip:8080 - ``` - -### Option 2: Manual Installation - -If you prefer a manual approach or encounter issues with the automated install: - -1. **Create installation directory**: - ```bash - mkdir -p ~/wol-server/templates - ``` - -2. **Transfer and install program files**: - ```bash - # Copy the executable - cp wol-server-arm6 ~/wol-server/wol-server - chmod +x ~/wol-server/wol-server - - # Copy template files - cp templates/* ~/wol-server/templates/ - - # Create .env file - cat > ~/wol-server/.env << EOL + cat > .env << EOL + # Server Configuration SERVER_NAME=pippo SERVER_USER=root - MAC_ADDRESS=aa:bb:cc:dd:ee:ff + MAC_ADDRESS=aa:aa:aa:aa:aa:aa PORT=8080 EOL ``` -3. **Install service**: +4. **Run the deployment script**: ```bash - sudo cp wol-server.service /etc/systemd/system/ - sudo systemctl daemon-reload - sudo systemctl enable wol-server - sudo systemctl start wol-server + ./deploy.sh ``` -4. **Install required dependencies**: - ```bash - sudo apt-get update - sudo apt-get install -y wakeonlan sshpass - ``` +### Method 2: Build from Source + +If you prefer to build the application from source: + +```bash +# Install Go (if not already installed) +sudo apt update +sudo apt install golang-go + +# Clone the repository +git clone https://github.com/thisloke/wol-server.git +cd wol-server + +# Install dependencies +go get github.com/joho/godotenv + +# Create a .env file for configuration +cat > .env << EOL +# Server Configuration +SERVER_NAME=pippo +SERVER_USER=root +MAC_ADDRESS=aa:aa:aa:aa:aa:aa +PORT=8080 +EOL + +# Build the application +go build -o wol-server + +# Create installation directory +mkdir -p ~/wol-server + +# Copy the binary and config +cp wol-server ~/wol-server/ +cp .env ~/wol-server/ +chmod +x ~/wol-server/wol-server + +# Create and install systemd service +sudo bash -c 'cat > /etc/systemd/system/wol-server.service << EOL +[Unit] +Description=WOL Server Go Application +After=network.target + +[Service] +User=pi +WorkingDirectory=/home/pi/wol-server +ExecStart=/home/pi/wol-server/wol-server +Restart=always + +[Install] +WantedBy=multi-user.target +EOL' + +# Enable and start the service +sudo systemctl daemon-reload +sudo systemctl enable wol-server +sudo systemctl start wol-server +``` + +### Method 3: Manual Installation + +If you encounter issues with the deployment script: + +```bash +# Create the application directory +mkdir -p ~/wol-server + +# Copy the binary and rename it +cp wol-server-arm6 ~/wol-server/wol-server +chmod +x ~/wol-server/wol-server + +# Copy the .env file +cp .env ~/wol-server/ + +# Install the service file +sudo cp wol-server.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable wol-server +sudo systemctl start wol-server +``` ## Configuration -The application can be configured by editing the `.env` file in the installation directory: - -```bash -nano ~/wol-server/.env -``` +WOL-server can be configured using environment variables or a `.env` file in the application directory. ### Available Configuration Options -| Setting | Description | Default | -|---------|-------------|---------| -| `SERVER_NAME` | Hostname/IP of target server | pippo | -| `SERVER_USER` | SSH username for shutdown | root | -| `MAC_ADDRESS` | MAC address for Wake-on-LAN | aa:bb:cc:dd:ee:ff | -| `PORT` | Web interface port | 8080 | -| `SHUTDOWN_PASSWORD` | Password for all shutdown operations | None | -| `REFRESH_INTERVAL` | UI refresh interval in seconds | 60 | +| Environment Variable | Description | Default Value | +|----------------------|-------------|---------------| +| `SERVER_NAME` | Name of the server to ping/wake | `pippo` | +| `SERVER_USER` | SSH username for remote commands | `root` | +| `MAC_ADDRESS` | MAC address of the target server | `aa:aa:aa:aa:aa:aa` | +| `PORT` | The port number for the web server | `8080` | -The scheduled backup window configuration is stored in `schedule.json` in the installation directory. It includes the start time, end time, and frequency settings. +### Customizing Your Configuration + +You can edit the `.env` file to modify the application's behavior: + +```bash +# Navigate to the installation directory +cd ~/wol-server + +# Edit the .env file +nano .env +``` + +After modifying the configuration, restart the service: -After changing configuration, restart the service: ```bash sudo systemctl restart wol-server ``` ## Usage -### Accessing the Interface - -Open a web browser and navigate to: +Once installed, the WOL server will be accessible at: ``` http://your-pi-ip:8080 ``` +(or whatever port you've configured) -### Features +### Using the Web Interface -- **Status Checking**: The interface shows the current status (Online/Offline) -- **Booting**: Click the "Boot" button to send a WOL magic packet -- **Shutting Down**: Click "Shutdown" and enter your SSH password when prompted -- **Scheduled Backup Window**: Configure automatic server startup and shutdown on a regular schedule +1. **Wake Server**: Click the "Boot" button to send a Wake-on-LAN magic packet to the configured MAC address +2. **Shut Down Server**: Click the "Shutdown" button, confirm, and enter your password if required -#### Using Scheduled Backup Window +## Service Management -1. Click "Configure Schedule" in the Scheduled Backup Window section -2. Enter your desired start and end times (in 24-hour format) -3. Select a frequency (daily, every 2 days, weekly, or monthly) -4. Optionally, enable "Auto Shutdown" (requires SHUTDOWN_PASSWORD in .env file) -5. Click "Save Schedule" to activate -6. The server will automatically boot at the start time and: - - If auto shutdown is enabled: automatically shut down at the end time - - If auto shutdown is disabled: remain on until manually shut down -7. To modify an existing schedule, click "Edit Schedule" -8. To disable, click "Disable Schedule" from the main interface - -**Note:** All shutdown operations (manual and scheduled) use the SHUTDOWN_PASSWORD from your .env file. - -#### Auto Shutdown Feature - -The auto shutdown feature provides several advantages: -- Saves power by ensuring the server only runs during scheduled backup periods -- Prevents the server from accidentally remaining on after backups are complete -- Fully automates the backup window process -- Smart protection: only shuts down servers that were started by the scheduler - -**Requirements for Shutdown Operations:** -1. Set the `SHUTDOWN_PASSWORD` in your .env file -2. The SSH server must be properly configured on the target server -3. The user account specified in the configuration must have sudo privileges -4. The password must be correct for the specified user account -5. The server must allow password authentication via SSH - -**Troubleshooting Shutdown Operations:** -- If shutdown fails, check the logs for specific error messages -- Ensure `sshpass` is installed on your Raspberry Pi (`sudo apt-get install sshpass`) -- Verify you can manually SSH to the server with the provided credentials -- Confirm the user has sudo privileges to run the shutdown command -- Check if the server requires SSH key authentication instead of password -- Verify the SHUTDOWN_PASSWORD is correctly set in your .env file - -#### Auto-Refreshing UI - -The web interface automatically refreshes every minute (or according to the REFRESH_INTERVAL setting) to show the current server status. This ensures you always see up-to-date information without having to manually refresh the page. - -## Maintenance - -### Checking Service Status +Control the WOL server using standard systemd commands: ```bash +# Check service status sudo systemctl status wol-server + +# Stop the service +sudo systemctl stop wol-server + +# Start the service +sudo systemctl start wol-server + +# Restart the service +sudo systemctl restart wol-server + +# View logs +journalctl -u wol-server -f ``` -### Viewing Logs +## Updating to a New Version + +To update to a new version: ```bash -sudo journalctl -u wol-server -f +# Navigate to a temporary directory +mkdir -p ~/wol-update +cd ~/wol-update + +# Download the latest release (replace v7 with the latest version number) +wget https://github.com/thisloke/wol-server/releases/download/v7/wol-server-arm6 +chmod +x wol-server-arm6 + +# Stop the service +sudo systemctl stop wol-server + +# Replace the binary +cp wol-server-arm6 ~/wol-server/wol-server + +# Start the service +sudo systemctl start wol-server + +# Clean up +cd ~ +rm -rf ~/wol-update ``` -### Updating - -To update to a newer version: - -1. Download and transfer the latest release -2. Stop the service: - ```bash - sudo systemctl stop wol-server - ``` -3. Extract the new files: - ```bash - tar -xzf wol-server.tar.gz - ``` -4. Run the install script: - ```bash - ./install.sh - ``` - ## Troubleshooting -### Service Won't Start +### Checking Your Raspberry Pi Architecture + +If you need to verify which version of the application you should use: -Check for template errors: ```bash -ls -la ~/wol-server/templates/ +uname -m ``` -Verify the .env file exists: +This will output your Pi's architecture: +- `armv6l`: Use the `wol-server-arm6` binary (Pi Zero, Pi 1) +- `armv7l`: Use the `wol-server-arm7` binary (Pi 2, Pi 3) +- `aarch64`: Use the `wol-server-arm64` binary (64-bit Pi 3, Pi 4) + +### Service Not Starting + +If the service doesn't start properly, check the logs: + ```bash -cat ~/wol-server/.env +journalctl -u wol-server -e ``` -### Boot Command Not Working +### Checking Configuration -1. Ensure `wakeonlan` is installed: - ```bash - which wakeonlan || sudo apt-get install wakeonlan - ``` -2. Verify the MAC address is correct in your .env file -3. Make sure the target device is properly configured for Wake-on-LAN +Verify that your configuration is being properly loaded: -### Shutdown Not Working - -1. Verify `sshpass` is installed: - ```bash - which sshpass || sudo apt-get install sshpass - ``` -2. Check that the SERVER_USER setting in .env is correct -3. Ensure SSH access is working between your Pi and the target server - -## Advanced Configuration - -### Running on a Different Port - -Edit the `.env` file: ```bash -echo "PORT=8181" >> ~/wol-server/.env +# View the environment variables being used +sudo systemctl status wol-server ``` -### Multiple Target Machines +Look for a line in the output that shows the loaded configuration values. + +### Permission Issues + +Make sure the binary has execute permissions: -To control multiple devices, you can install multiple instances: ```bash -# Create a second instance -mkdir -p ~/wol-server2/templates -cp -r ~/wol-server/templates/* ~/wol-server2/templates/ -cp ~/wol-server/wol-server ~/wol-server2/ - -# Different config -cat > ~/wol-server2/.env << EOL -SERVER_NAME=server2 -SERVER_USER=admin -MAC_ADDRESS=aa:bb:cc:dd:ee:ff -PORT=8081 -EOL - -# Create a new service -sudo cp /etc/systemd/system/wol-server.service /etc/systemd/system/wol-server2.service -sudo sed -i 's|/home/pi/wol-server|/home/pi/wol-server2|g' /etc/systemd/system/wol-server2.service -sudo systemctl daemon-reload -sudo systemctl enable wol-server2 -sudo systemctl start wol-server2 +chmod +x ~/wol-server/wol-server ``` -## Project Information +### Can't Download Release Files -Designed for use with Raspberry Pi to provide a simple way to manage servers and devices on your local network. The web interface makes it easy to power on and off machines without having to remember MAC addresses or commands. +If you're having trouble downloading the release files directly, you can also: -### Contributing +1. Download the files on your computer +2. Transfer them to your Raspberry Pi using SCP, SFTP, or a USB drive +3. Continue with the installation steps as described above -Contributions are welcome! Feel free to submit pull requests or open issues to help improve this project. +## Contributing -### License +Contributions are welcome! Feel free to submit pull requests or open issues for bugs and feature requests. + +## License This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/handlers.go b/handlers.go index 684c558..0c4333e 100644 --- a/handlers.go +++ b/handlers.go @@ -4,7 +4,6 @@ import ( "log" "net/http" "runtime" - "time" ) // Handle the root route - show status @@ -14,32 +13,21 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { return } - // Add cache control headers to prevent caching - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - online := isServerOnline() status := "Online" - color := "#4caf50" // Material green + color := "#4caf50" // Material green if !online { status = "Offline" - color = "#d32f2f" // Material red + color = "#d32f2f" // Material red } - // Get current schedule configuration - scheduleConfig := GetScheduleConfig() - data := StatusData{ - Server: serverName, - Status: status, - Color: color, - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "", - Schedule: scheduleConfig, - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: status, + Color: color, + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + ErrorMessage: "", } if err := tmpl.Execute(w, data); err != nil { @@ -59,14 +47,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { // Display booting status data := StatusData{ - Server: serverName, - Status: "Booting", - Color: "#607d8b", // Material blue-gray - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Booting", + Color: "#607d8b", // Material blue-gray + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -75,14 +60,11 @@ func bootHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already online data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -98,14 +80,11 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { if !online { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -114,53 +93,42 @@ func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Check if shutdown password is set - if shutdownPassword == "" { - // Show error about missing password - data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ConfirmShutdown: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment. Please set it in the .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, - } - if err := tmpl.Execute(w, data); err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - log.Printf("Template render error: %v", err) - } - return - } - - // Show confirmation dialog - we'll use the password from .env + // Show confirmation dialog data := StatusData{ Server: serverName, Status: "Online", - Color: "#4caf50", // Material green + Color: "#4caf50", // Material green IsTestMode: runtime.GOOS == "darwin", ConfirmShutdown: true, - AskPassword: false, // Make sure we don't ask for password - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), + AskPassword: false, } - - // Notify the user if password is not configured - if shutdownPassword == "" { - data.ErrorMessage = "SHUTDOWN_PASSWORD not set in environment. Shutdown may fail." - } - if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) log.Printf("Template render error: %v", err) } } -// Handle shutdown confirmation without password -// enterPasswordHandler function removed - we now use the password from .env directly +// Handle password entry for shutdown +func enterPasswordHandler(w http.ResponseWriter, r *http.Request) { + if !isServerOnline() { + // Server is already offline, redirect to home + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Show password entry dialog + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} // Handle actual shutdown request func shutdownHandler(w http.ResponseWriter, r *http.Request) { @@ -170,20 +138,25 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { return } - // Use the password from environment variable - if shutdownPassword == "" { - log.Printf("SHUTDOWN_PASSWORD not set in environment, cannot perform shutdown") - // Show error message + // Parse form data to get password + if err := r.ParseForm(); err != nil { + log.Printf("Error parsing form: %v", err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Get password from form + password := r.FormValue("password") + + if password == "" { + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - ErrorMessage: "SHUTDOWN_PASSWORD not set in environment", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Password cannot be empty", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -193,22 +166,19 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } if isServerOnline() { - // Shutdown the server using the password from .env file - err := shutdownServer(shutdownPassword) + // Shutdown the server + err := shutdownServer(password) if err != nil { log.Printf("Error shutting down server: %v", err) - // Show error message + // Show password form again with error data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, // No longer asking for password - ErrorMessage: "Failed to shutdown server. Please check the password in .env file.", - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Failed to shutdown server. Please check your password.", } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -219,14 +189,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { // Display shutting down status data := StatusData{ - Server: serverName, - Status: "Shutting down", - Color: "#5d4037", // Material brown - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Shutting down", + Color: "#5d4037", // Material brown + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) @@ -235,14 +202,11 @@ func shutdownHandler(w http.ResponseWriter, r *http.Request) { } else { // Server is already offline data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - AskPassword: false, - Schedule: GetScheduleConfig(), - LastUpdated: time.Now().Format("2006-01-02 15:04:05"), - RefreshInterval: refreshInterval, + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, } if err := tmpl.Execute(w, data); err != nil { http.Error(w, "Failed to render template", http.StatusInternalServerError) diff --git a/main.go b/main.go index d4b78c6..7bb50e8 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,21 @@ package main import ( - "bytes" - "encoding/json" "fmt" "log" "net/http" "os" - "os/exec" "runtime" - "strconv" - "time" "github.com/joho/godotenv" ) // Default values var ( - serverName = "server" // Server to ping - serverUser = "root" // SSH username - macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server - port = "8080" // Port to listen on - refreshInterval = 60 // UI refresh interval in seconds + serverName = "server" // Server to ping + serverUser = "root" // SSH username + macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server + port = "8080" // Port to listen on ) func loadEnvVariables() { @@ -47,15 +41,8 @@ func loadEnvVariables() { port = envPort } - // Load refresh interval if set - if envRefresh := os.Getenv("REFRESH_INTERVAL"); envRefresh != "" { - if val, err := strconv.Atoi(envRefresh); err == nil && val > 0 { - refreshInterval = val - } - } - - log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s, REFRESH=%d", - serverName, serverUser, macAddress, port, refreshInterval) + log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s", + serverName, serverUser, macAddress, port) } func main() { @@ -67,24 +54,13 @@ func main() { log.Fatalf("Failed to setup template: %v", err) } - // Verify schedule configuration and clean up stale schedule data if needed - verifyScheduleConfig() - - // Setup a ticker to check schedule and perform actions - go runScheduleChecker() - // Register route handlers http.HandleFunc("/", indexHandler) http.HandleFunc("/boot", bootHandler) http.HandleFunc("/confirm-shutdown", confirmShutdownHandler) - // Password is now taken directly from .env file + http.HandleFunc("/enter-password", enterPasswordHandler) http.HandleFunc("/shutdown", shutdownHandler) - // Schedule API endpoints - http.HandleFunc("/api/schedule", scheduleHandler) - // API shutdown endpoint - http.HandleFunc("/api/shutdown", apiShutdownHandler) - // Start the server listenAddr := fmt.Sprintf(":%s", port) log.Printf("Starting WOL Server on http://localhost%s", listenAddr) @@ -97,364 +73,3 @@ func main() { log.Fatalf("Server failed to start: %v", err) } } - -// API Shutdown handler - shuts down the server with password from environment -func apiShutdownHandler(w http.ResponseWriter, r *http.Request) { - // Add cache control headers to prevent caching - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Pragma", "no-cache") - w.Header().Set("Expires", "0") - // Set content type for JSON response - w.Header().Set("Content-Type", "application/json") - - // Only allow POST requests - if r.Method != "POST" { - w.WriteHeader(http.StatusMethodNotAllowed) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Method not allowed. Use POST.", - }) - return - } - - // Check if shutdown password is available in environment - if shutdownPassword == "" { - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "SHUTDOWN_PASSWORD not set in environment", - }) - return - } - - // Check if server is online before attempting shutdown - if !isServerOnline() { - // Server is already offline - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Server is already offline", - }) - return - } - - // Try to shut down the server using the password from environment - err := shutdownServer(shutdownPassword) - if err != nil { - // Shutdown command failed - w.WriteHeader(http.StatusInternalServerError) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": false, - "error": "Failed to shutdown server: " + err.Error(), - }) - log.Printf("API shutdown failed: %v", err) - return - } - - // Shutdown initiated successfully - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]interface{}{ - "success": true, - "message": "Server shutdown initiated", - }) - log.Printf("API shutdown successful") -} - -// Handle schedule API requests -func scheduleHandler(w http.ResponseWriter, r *http.Request) { - // Set content type - w.Header().Set("Content-Type", "application/json") - - // Handle GET request - return current schedule - if r.Method == "GET" { - data, err := json.Marshal(GetScheduleConfig()) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError) - return - } - w.Write(data) - return - } - - // Handle POST request - update schedule - if r.Method == "POST" { - var newConfig ScheduleConfig - err := json.NewDecoder(r.Body).Decode(&newConfig) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to parse request body: %v"}`, err), http.StatusBadRequest) - return - } - - // Validate the schedule data - if newConfig.Enabled { - // Validate time format (HH:MM) - _, err = time.Parse("15:04", newConfig.StartTime) - if err != nil { - http.Error(w, `{"error": "Invalid start time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest) - return - } - - _, err = time.Parse("15:04", newConfig.EndTime) - if err != nil { - http.Error(w, `{"error": "Invalid end time format. Use 24-hour format (HH:MM)"}`, http.StatusBadRequest) - return - } - - // Validate frequency - validFrequencies := map[string]bool{ - "daily": true, - "every2days": true, - "weekly": true, - "monthly": true, - } - - if !validFrequencies[newConfig.Frequency] { - http.Error(w, `{"error": "Invalid frequency. Use 'daily', 'every2days', 'weekly', or 'monthly'"}`, http.StatusBadRequest) - return - } - - // Reset lastRun if it wasn't set - if newConfig.LastRun == "" { - newConfig.LastRun = "" - } - - // If auto shutdown is enabled, make sure we have a password in env - if newConfig.AutoShutdown && shutdownPassword == "" { - http.Error(w, `{"error": "SHUTDOWN_PASSWORD not set in environment. Please set it before enabling auto-shutdown"}`, http.StatusBadRequest) - return - } - - // Check if SSH connection can be established with the password - if newConfig.AutoShutdown && shutdownPassword != "" { - log.Printf("Testing SSH connection to %s with provided password", serverName) - - // We'll just check if the server is reachable first - if !isServerOnline() { - log.Printf("Server %s is not online, can't test SSH connection", serverName) - } else { - // Try to run a harmless command to test SSH connection - cmd := exec.Command("sshpass", "-p", shutdownPassword, "ssh", - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "LogLevel=ERROR", - "-o", "ConnectTimeout=5", - fmt.Sprintf("%s@%s", serverUser, serverName), - "echo", "SSH connection test successful") - - var stderr bytes.Buffer - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - log.Printf("SSH connection test failed: %v - %s", err, stderr.String()) - // We don't prevent saving the config even if test fails - // Just log a warning for now - log.Printf("WARNING: Auto shutdown may not work with the provided password") - } else { - log.Printf("SSH connection test successful") - } - } - } - } - - // Save the new configuration - err = UpdateScheduleConfig(newConfig) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to save schedule config: %v"}`, err), http.StatusInternalServerError) - return - } - - // Return the updated config - data, err := json.Marshal(GetScheduleConfig()) - if err != nil { - http.Error(w, fmt.Sprintf(`{"error": "Failed to marshal schedule data: %v"}`, err), http.StatusInternalServerError) - return - } - w.Write(data) - return - } - - // Method not allowed - http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed) -} - -// Verify and clean up schedule configuration -func verifyScheduleConfig() { - // If schedule is enabled, validate all required fields - if scheduleConfig.Enabled { - log.Println("Verifying schedule configuration...") - log.Printf("Current config: StartTime=%s, EndTime=%s, Frequency=%s, AutoShutdown=%v", - scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown) - - // Check for valid time formats - _, startErr := time.Parse("15:04", scheduleConfig.StartTime) - _, endErr := time.Parse("15:04", scheduleConfig.EndTime) - - if startErr != nil || endErr != nil || scheduleConfig.StartTime == "" || scheduleConfig.EndTime == "" { - log.Println("Warning: Invalid time format in schedule configuration, disabling schedule") - scheduleConfig.Enabled = false - UpdateScheduleConfig(scheduleConfig) - return - } - - // Immediately check if we need to boot ONLY at exact start time - now := time.Now() - currentTimeStr := now.Format("15:04") - - // Check ONLY if current time EXACTLY matches start time - if currentTimeStr == scheduleConfig.StartTime && ShouldRunToday(now) { - log.Printf("STARTUP MATCH: Current time %s matches start time EXACTLY, attempting boot", currentTimeStr) - if !isServerOnline() { - sendWakeOnLAN() - // Mark that the server was started by the scheduler - scheduleConfig.StartedBySchedule = true - scheduleConfig.LastRun = now.Format(time.RFC3339) - UpdateScheduleConfig(scheduleConfig) - } - } - - // Check for valid frequency - validFrequencies := map[string]bool{ - "daily": true, - "every2days": true, - "weekly": true, - "monthly": true, - } - - if !validFrequencies[scheduleConfig.Frequency] { - log.Println("Warning: Invalid frequency in schedule configuration, setting to daily") - scheduleConfig.Frequency = "daily" - UpdateScheduleConfig(scheduleConfig) - } - - log.Printf("Schedule configuration verified: Start=%s, End=%s, Frequency=%s", - scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency) - } -} - -// Run a periodic check of schedule and take appropriate actions -func runScheduleChecker() { - // Define the checkScheduleOnce function - checkScheduleOnce := func() { - // Only check exact times for schedule actions, don't use window logic - now := time.Now() - currentTimeStr := now.Format("15:04") - serverIsOn := isServerOnline() - - // Log schedule status (debug level) - if scheduleConfig.Enabled { - log.Printf("Schedule check: Current=%s, Start=%s, End=%s, LastRun=%s", - currentTimeStr, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.LastRun) - - // Only act at exact start or end times - // EXACT START TIME MATCH - Try to boot server - if currentTimeStr == scheduleConfig.StartTime && !serverIsOn && ShouldRunToday(now) { - log.Println("EXACT START TIME: Initiating boot sequence...") - - // Try multiple times to boot with small delays between attempts - for attempt := 1; attempt <= 3; attempt++ { - log.Printf("Boot attempt %d/3", attempt) - err := sendWakeOnLAN() - if err != nil { - log.Printf("Error booting server from schedule: %v", err) - } else { - log.Println("Schedule: Boot command sent successfully") - // Mark that server was started by scheduler - scheduleConfig.StartedBySchedule = true - scheduleConfig.LastRun = now.Format(time.RFC3339) - UpdateScheduleConfig(scheduleConfig) - } - - // Check if server came online - time.Sleep(3 * time.Second) // Extended wait time for boot check - if isServerOnline() { - log.Println("Server successfully booted!") - break - } - - // Short delay before next attempt - if attempt < 3 { - time.Sleep(1 * time.Second) - } - } - // EXACT END TIME MATCH - Try to shutdown server - } else if currentTimeStr == scheduleConfig.EndTime && serverIsOn { - // Check if auto-shutdown is enabled - if scheduleConfig.AutoShutdown && shutdownPassword != "" && scheduleConfig.StartedBySchedule { - log.Println("EXACT END TIME: Attempting auto-shutdown") - - // Try multiple times to shut down the server - var shutdownSuccessful bool - for attempt := 1; attempt <= 3; attempt++ { - log.Printf("Auto shutdown attempt %d/3", attempt) - err := shutdownServer(shutdownPassword) - if err != nil { - log.Printf("Auto shutdown attempt %d failed: %v", attempt, err) - if attempt < 3 { - time.Sleep(3 * time.Second) - } - } else { - log.Printf("Auto shutdown initiated successfully on attempt %d", attempt) - shutdownSuccessful = true - break - } - } - - if !shutdownSuccessful { - log.Printf("All auto shutdown attempts failed") - } - } - } else { - // No action at non-exact times, just log status - if serverIsOn && scheduleConfig.StartedBySchedule && currentTimeStr > scheduleConfig.EndTime { - log.Printf("Server is still online after end time %s - waiting for next exact end time match", scheduleConfig.EndTime) - } - } - } - - // Update last run timestamp if we've passed the end time - // This helps track when the schedule was last active - currentConfig := GetScheduleConfig() - nowTime := time.Now() - currentTimeString := nowTime.Format("15:04") - if currentConfig.Enabled && currentTimeString > currentConfig.EndTime && currentConfig.LastRun != "" { - lastRun, err := time.Parse(time.RFC3339, currentConfig.LastRun) - if err == nil { - // If it's been more than a day since the last update, reset the timestamp - // This allows the schedule to run again based on frequency - if time.Since(lastRun) > 24*time.Hour { - log.Println("Schedule: Resetting last run timestamp for next scheduled run") - currentConfig.LastRun = "" - UpdateScheduleConfig(currentConfig) - } - } - } - } - - // Use a slightly shorter interval for more responsive scheduling - // First check immediately at startup - checkScheduleOnce() - - // Then set up regular checks - ticker := time.NewTicker(5 * time.Second) - defer ticker.Stop() - - log.Println("Schedule checker started - checking every 5 seconds") - log.Printf("Current schedule: enabled=%v, startTime=%s, endTime=%s, frequency=%s, autoShutdown=%v", - scheduleConfig.Enabled, scheduleConfig.StartTime, scheduleConfig.EndTime, scheduleConfig.Frequency, scheduleConfig.AutoShutdown) - - for { - func() { - // Recover from any panics that might occur in the schedule checker - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in schedule checker: %v", r) - } - }() - - checkScheduleOnce() - }() - - // Wait for next tick - <-ticker.C - } -} diff --git a/schedule.json b/schedule.json deleted file mode 100644 index a07df7d..0000000 --- a/schedule.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "enabled": true, - "startTime": "13:46", - "endTime": "13:49", - "frequency": "daily", - "lastRun": "2025-09-05T13:46:41+02:00", - "autoShutdown": true, - "startedBySchedule": true -} \ No newline at end of file diff --git a/templates/status.html b/templates/status.html index 1aae2aa..2c00eaa 100644 --- a/templates/status.html +++ b/templates/status.html @@ -1,773 +1,402 @@ - + -
- - - + + +- Start Time: - {{.Schedule.StartTime}} -
-- End Time: - {{.Schedule.EndTime}} -
-- Frequency: - {{.Schedule.Frequency}} -
-- Auto Shutdown: - {{if .Schedule.AutoShutdown}} - Enabled - {{else}} - Disabled - {{end}} -
-- Last Update: - {{.LastUpdated}} -
-