Initialize Go module
This commit is contained in:
parent
f9549a10bb
commit
1397fffeca
10 changed files with 1101 additions and 537 deletions
4
.env
Normal file
4
.env
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
SERVER_NAME=delemaco
|
||||
SERVER_USER=root
|
||||
MAC_ADDRESS=b8:cb:29:a1:f3:88
|
||||
PORT=8080
|
||||
4
.env.sample
Normal file
4
.env.sample
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
SERVER_NAME=pippo
|
||||
SERVER_USER=root
|
||||
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
||||
PORT=8080
|
||||
178
.github/workflows/build.yml
vendored
178
.github/workflows/build.yml
vendored
|
|
@ -43,63 +43,152 @@ jobs:
|
|||
|
||||
- name: Create deployment script
|
||||
run: |
|
||||
cat > deploy.sh << 'EOL'
|
||||
cat > install.sh << 'EOL'
|
||||
#!/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"
|
||||
# Installation directory
|
||||
INSTALL_DIR=~/wol-server
|
||||
|
||||
# Select the right binary based on processor architecture
|
||||
ARCH=$(uname -m)
|
||||
echo "Architecture: $ARCH"
|
||||
echo "Creating installation directory..."
|
||||
mkdir -p $INSTALL_DIR
|
||||
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
|
||||
echo "Installing application..."
|
||||
cp wol-server-arm6 $INSTALL_DIR/wol-server
|
||||
chmod +x $INSTALL_DIR/wol-server
|
||||
|
||||
echo "Creating directory..."
|
||||
mkdir -p ~/wol-server
|
||||
echo "Installing template files..."
|
||||
cp -r templates/* $INSTALL_DIR/templates/
|
||||
|
||||
echo "Copying application..."
|
||||
cp wol-server ~/wol-server/
|
||||
chmod +x ~/wol-server/wol-server
|
||||
|
||||
echo "Installing service..."
|
||||
echo "Installing system service..."
|
||||
sudo cp wol-server.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server
|
||||
|
||||
# Install required dependencies
|
||||
echo "Installing dependencies..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y wakeonlan sshpass
|
||||
|
||||
# Start the service
|
||||
echo "Starting service..."
|
||||
sudo systemctl restart wol-server
|
||||
|
||||
echo "Deployment complete!"
|
||||
echo "Service status:"
|
||||
sudo systemctl status wol-server
|
||||
echo "==========================================="
|
||||
echo "Installation complete!"
|
||||
echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
|
||||
echo "==========================================="
|
||||
EOL
|
||||
|
||||
chmod +x deploy.sh
|
||||
chmod +x install.sh
|
||||
|
||||
- name: Create archive for each platform
|
||||
- name: Create README with installation instructions
|
||||
run: |
|
||||
# ARMv6 package
|
||||
mkdir -p 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 .
|
||||
cat > INSTALL.md << 'EOL'
|
||||
# WOL Server Installation Guide
|
||||
|
||||
# All-in-one package
|
||||
mkdir -p all-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 .
|
||||
This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Raspberry Pi running Raspberry Pi OS (Raspbian)
|
||||
- SSH access to your Pi
|
||||
- SCP or SFTP capability to transfer files
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Transfer Files to Raspberry Pi
|
||||
|
||||
**Option 1: Using SCP from your computer**
|
||||
|
||||
```bash
|
||||
# Replace with your Pi's IP address
|
||||
PI_IP=192.168.1.100
|
||||
|
||||
# Transfer the installation package
|
||||
scp wol-server.tar.gz pi@$PI_IP:~/
|
||||
```
|
||||
|
||||
**Option 2: Using SFTP client**
|
||||
|
||||
Use a tool like FileZilla, WinSCP, or Cyberduck to transfer the `wol-server.tar.gz` file to your Raspberry Pi.
|
||||
|
||||
### 2. SSH into your Raspberry Pi
|
||||
|
||||
```bash
|
||||
ssh pi@192.168.1.100
|
||||
```
|
||||
|
||||
### 3. Extract and Install
|
||||
|
||||
```bash
|
||||
# Navigate to home directory
|
||||
cd ~
|
||||
|
||||
# Extract the archive
|
||||
tar -xzf wol-server.tar.gz
|
||||
|
||||
# Run the installation script
|
||||
./install.sh
|
||||
```
|
||||
|
||||
### 4. Test the Installation
|
||||
|
||||
Open a web browser and navigate to:
|
||||
```
|
||||
http://[your-pi-ip]:8080
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the service fails to start, check the logs:
|
||||
```bash
|
||||
sudo systemctl status wol-server
|
||||
```
|
||||
|
||||
If template errors occur, ensure the template files were copied correctly:
|
||||
```bash
|
||||
ls -la ~/wol-server/templates/
|
||||
```
|
||||
|
||||
## Manual Installation (if needed)
|
||||
|
||||
If you encounter issues with the automated install:
|
||||
|
||||
```bash
|
||||
# Create directories
|
||||
mkdir -p ~/wol-server/templates
|
||||
|
||||
# Copy files manually
|
||||
cp wol-server-arm6 ~/wol-server/wol-server
|
||||
chmod +x ~/wol-server/wol-server
|
||||
cp templates/* ~/wol-server/templates/
|
||||
sudo cp wol-server.service /etc/systemd/system/
|
||||
|
||||
# Install dependencies
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wakeonlan sshpass
|
||||
|
||||
# Enable and start service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server
|
||||
sudo systemctl start wol-server
|
||||
```
|
||||
EOL
|
||||
|
||||
- name: Create all-in-one package
|
||||
run: |
|
||||
# Create a single package with everything needed
|
||||
mkdir -p package
|
||||
cp wol-server-arm6 package/
|
||||
cp wol-server.service package/
|
||||
cp install.sh package/
|
||||
cp -r templates package/
|
||||
cp INSTALL.md package/
|
||||
|
||||
# Create the tarball
|
||||
tar -czf wol-server.tar.gz -C package .
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
|
|
@ -111,10 +200,7 @@ jobs:
|
|||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
wol-server-arm6
|
||||
wol-server.service
|
||||
deploy.sh
|
||||
wol-server-armv6.tar.gz
|
||||
wol-server-all.tar.gz
|
||||
wol-server.tar.gz
|
||||
INSTALL.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
283
README.md
283
README.md
|
|
@ -1,147 +1,234 @@
|
|||

|
||||
# WOL Server - Wake-on-LAN Control Panel for Raspberry Pi
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Status Monitoring**: Real-time status checks to determine if your server is online or offline
|
||||
- **Wake-on-LAN**: Boot your server remotely with the click of a button
|
||||
- **Remote Shutdown**: Safely shut down your server when it's not needed
|
||||
- **Responsive UI**: Simple, mobile-friendly interface with color-coded status indicators
|
||||
- **Lightweight**: Built with Go for minimal resource usage, perfect for Raspberry Pi Zero
|
||||
|
||||
## Requirements
|
||||
|
||||
- Raspberry Pi (Zero, 2, 3, 4, etc.)
|
||||
- Go (version 1.16 or higher)
|
||||
- wakeonlan utility
|
||||
- SSH access to the target server (for shutdown functionality)
|
||||
- Target server configured for Wake-on-LAN
|
||||
- **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI
|
||||
- **Status Monitoring**: Check if your target device is online
|
||||
- **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
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install Dependencies
|
||||
### Prerequisites
|
||||
|
||||
- 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:
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install golang-go wakeonlan
|
||||
scp wol-server.tar.gz pi@your-pi-ip:~/
|
||||
```
|
||||
|
||||
### 2. Clone the Repository
|
||||
|
||||
3. **SSH into your Raspberry Pi**:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/wol-server.git
|
||||
cd wol-server
|
||||
ssh pi@your-pi-ip
|
||||
```
|
||||
|
||||
### 3. Configure the Application
|
||||
|
||||
Edit the constants in `main.go` to match your server:
|
||||
|
||||
```go
|
||||
const (
|
||||
serverName = "yourserver" // Hostname or IP address of your server
|
||||
macAddress = "xx:xx:xx:xx:xx:xx" // MAC address of your server's network interface
|
||||
port = "8080" // Port to run the web application on
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Build the Application
|
||||
|
||||
4. **Install with a single command**:
|
||||
```bash
|
||||
go build -o wol-server
|
||||
tar -xzf wol-server.tar.gz && ./install.sh
|
||||
```
|
||||
|
||||
### 5. Set Up as a System Service
|
||||
5. **Access the web interface** at:
|
||||
```
|
||||
http://your-pi-ip:8080
|
||||
```
|
||||
|
||||
Create a systemd service file:
|
||||
### Option 2: Manual Installation
|
||||
|
||||
If you prefer a manual approach or encounter issues with the automated install:
|
||||
|
||||
1. **Create installation directory**:
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/wol-server.service
|
||||
mkdir -p ~/wol-server/templates
|
||||
```
|
||||
|
||||
Add the following content (adjust paths if needed):
|
||||
|
||||
```
|
||||
[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
|
||||
```
|
||||
|
||||
Enable and start the service:
|
||||
|
||||
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
|
||||
SERVER_NAME=pippo
|
||||
SERVER_USER=root
|
||||
MAC_ADDRESS=aa:bb:cc:dd:ee:ff
|
||||
PORT=8080
|
||||
EOL
|
||||
```
|
||||
|
||||
3. **Install service**:
|
||||
```bash
|
||||
sudo cp wol-server.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable wol-server
|
||||
sudo systemctl start wol-server
|
||||
```
|
||||
|
||||
## SSH Configuration for Remote Shutdown
|
||||
|
||||
For the shutdown functionality to work, you need to set up password-less SSH:
|
||||
|
||||
1. Generate an SSH key on your Raspberry Pi:
|
||||
4. **Install required dependencies**:
|
||||
```bash
|
||||
ssh-keygen -t rsa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y wakeonlan sshpass
|
||||
```
|
||||
|
||||
2. Copy the key to your target server:
|
||||
## Configuration
|
||||
|
||||
The application can be configured by editing the `.env` file in the installation directory:
|
||||
|
||||
```bash
|
||||
ssh-copy-id user@yourserver
|
||||
nano ~/wol-server/.env
|
||||
```
|
||||
|
||||
3. Configure sudo on the target server to allow password-less shutdown:
|
||||
```bash
|
||||
# On the target server, run:
|
||||
sudo visudo
|
||||
### Available Configuration Options
|
||||
|
||||
# Add this line (replacing 'user' with your username):
|
||||
user ALL=(ALL) NOPASSWD: /sbin/shutdown
|
||||
| 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 |
|
||||
|
||||
After changing configuration, restart the service:
|
||||
```bash
|
||||
sudo systemctl restart wol-server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Access the web interface by navigating to `http://raspberry-pi-ip:8080` in your browser.
|
||||
### Accessing the Interface
|
||||
|
||||
The interface provides:
|
||||
Open a web browser and navigate to:
|
||||
```
|
||||
http://your-pi-ip:8080
|
||||
```
|
||||
|
||||
- Current server status (Online/Offline)
|
||||
- Boot button - sends a Wake-on-LAN magic packet to your server
|
||||
- Shutdown button - safely shuts down your server via SSH
|
||||
- Refresh button - manually updates the status display
|
||||
### Features
|
||||
|
||||
- **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
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Checking Service Status
|
||||
|
||||
```bash
|
||||
sudo systemctl status wol-server
|
||||
```
|
||||
|
||||
### Viewing Logs
|
||||
|
||||
```bash
|
||||
sudo journalctl -u wol-server -f
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
- **Server won't boot**:
|
||||
- Verify that Wake-on-LAN is enabled in your server's BIOS/UEFI
|
||||
- Confirm the MAC address is correct
|
||||
- Check if your router blocks Wake-on-LAN packets
|
||||
### Service Won't Start
|
||||
|
||||
- **Shutdown doesn't work**:
|
||||
- Verify SSH key setup
|
||||
- Check sudo configuration on target server
|
||||
- Test manual SSH command: `ssh user@server sudo shutdown -h now`
|
||||
Check for template errors:
|
||||
```bash
|
||||
ls -la ~/wol-server/templates/
|
||||
```
|
||||
|
||||
- **Web interface not accessible**:
|
||||
- Ensure the service is running: `sudo systemctl status wol-server`
|
||||
- Check for firewall rules blocking port 8080
|
||||
- Verify the Raspberry Pi is connected to the network
|
||||
Verify the .env file exists:
|
||||
```bash
|
||||
cat ~/wol-server/.env
|
||||
```
|
||||
|
||||
## Contributing
|
||||
### Boot Command Not Working
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
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
|
||||
|
||||
## License
|
||||
### 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
|
||||
```
|
||||
|
||||
### Multiple Target Machines
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Project Information
|
||||
|
||||
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.
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions are welcome! Feel free to submit pull requests or open issues to help improve this project.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Inspired by the need for a simple, lightweight server power management tool
|
||||
- Thanks to the Go community for the excellent standard library that makes web development straightforward
|
||||
|
|
|
|||
5
go.mod
Normal file
5
go.mod
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/thisloke/wol-server
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/joho/godotenv v1.5.1 // indirect
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
216
handlers.go
Normal file
216
handlers.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Handle the root route - show status
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
online := isServerOnline()
|
||||
status := "Online"
|
||||
color := "#4caf50" // Material green
|
||||
if !online {
|
||||
status = "Offline"
|
||||
color = "#d32f2f" // Material red
|
||||
}
|
||||
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: status,
|
||||
Color: color,
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
AskPassword: false,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
|
||||
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 boot request
|
||||
func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !isServerOnline() {
|
||||
// Boot the server using wakeonlan
|
||||
err := sendWakeOnLAN()
|
||||
if err != nil {
|
||||
log.Printf("Error booting server: %v", err)
|
||||
}
|
||||
|
||||
// Display booting status
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Server is already online
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shutdown confirmation request
|
||||
func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||
online := isServerOnline()
|
||||
|
||||
if !online {
|
||||
// Server is already offline
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Online",
|
||||
Color: "#4caf50", // Material green
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
ConfirmShutdown: true,
|
||||
AskPassword: false,
|
||||
}
|
||||
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 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) {
|
||||
// Only process POST requests for security
|
||||
if r.Method != "POST" {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// 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: true,
|
||||
ErrorMessage: "Password cannot be empty",
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if isServerOnline() {
|
||||
// Shutdown the server
|
||||
err := shutdownServer(password)
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down server: %v", err)
|
||||
|
||||
// Show password form again with error
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Display shutting down status
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Server is already offline
|
||||
data := StatusData{
|
||||
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)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
410
main.go
410
main.go
|
|
@ -2,405 +2,63 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
const (
|
||||
serverName = "delemaco" // Server to ping
|
||||
macAddress = "b8:cb:29:a1:f3:88" // MAC address of the server
|
||||
// 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
|
||||
)
|
||||
|
||||
// StatusData holds data for the HTML template
|
||||
type StatusData struct {
|
||||
Server string
|
||||
Status string
|
||||
Color string
|
||||
IsTestMode bool
|
||||
func loadEnvVariables() {
|
||||
// Load .env file if it exists
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found, using default values")
|
||||
}
|
||||
|
||||
// Check if server is online
|
||||
func isServerOnline() bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// macOS and Linux have slightly different ping commands
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
|
||||
// Override defaults with environment variables if they exist
|
||||
if envServerName := os.Getenv("SERVER_NAME"); envServerName != "" {
|
||||
serverName = envServerName
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
if envServerUser := os.Getenv("SERVER_USER"); envServerUser != "" {
|
||||
serverUser = envServerUser
|
||||
}
|
||||
|
||||
// Send WOL packet
|
||||
func sendWakeOnLAN() error {
|
||||
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
||||
cmd := exec.Command("wakeonlan", macAddress)
|
||||
return cmd.Run()
|
||||
if envMacAddress := os.Getenv("MAC_ADDRESS"); envMacAddress != "" {
|
||||
macAddress = envMacAddress
|
||||
}
|
||||
|
||||
// Shutdown server
|
||||
func shutdownServer() error {
|
||||
|
||||
// Real shutdown command for Linux
|
||||
log.Printf("Sending shutdown command to %s", serverName)
|
||||
cmd := exec.Command("ssh", serverName, "sudo", "shutdown", "-h", "now")
|
||||
return cmd.Run()
|
||||
if envPort := os.Getenv("PORT"); envPort != "" {
|
||||
port = envPort
|
||||
}
|
||||
|
||||
// Handle the root route - show status
|
||||
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
online := isServerOnline()
|
||||
status := "Online"
|
||||
color := "#4caf50" // Material green
|
||||
if !online {
|
||||
status = "Offline"
|
||||
color = "#d32f2f" // Material red
|
||||
}
|
||||
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: status,
|
||||
Color: color,
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
|
||||
// Handle boot request
|
||||
func bootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !isServerOnline() {
|
||||
// Boot the server using wakeonlan
|
||||
err := sendWakeOnLAN()
|
||||
if err != nil {
|
||||
log.Printf("Error booting server: %v", err)
|
||||
}
|
||||
|
||||
// Display booting status
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Booting",
|
||||
Color: "#607d8b", // Material blue-gray
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
} else {
|
||||
// Server is already online
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Online",
|
||||
Color: "#4caf50", // Material green
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shutdown request
|
||||
func shutdownHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if isServerOnline() {
|
||||
// Shutdown the server
|
||||
err := shutdownServer()
|
||||
if err != nil {
|
||||
log.Printf("Error shutting down server: %v", err)
|
||||
}
|
||||
|
||||
// Display shutting down status
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Shutting down",
|
||||
Color: "#5d4037", // Material brown
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
} else {
|
||||
// Server is already offline
|
||||
data := StatusData{
|
||||
Server: serverName,
|
||||
Status: "Offline",
|
||||
Color: "#d32f2f", // Material red
|
||||
IsTestMode: runtime.GOOS == "darwin",
|
||||
}
|
||||
renderTemplate(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// Render the HTML template
|
||||
func renderTemplate(w http.ResponseWriter, data StatusData) {
|
||||
tmpl, err := template.New("status").Parse(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Status: {{.Server}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{.Color}};
|
||||
--text-color: white;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--hover-color: rgba(255, 255, 255, 0.1);
|
||||
--card-bg: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
transition: background-color 0.5s ease;
|
||||
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 20%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 20%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 15px 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
background-color: var(--shadow-color);
|
||||
transition: transform 0.2s ease, background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.button.refresh::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-99q0-13 8.5-21.5T770-800q13 0 21.5 8.5T800-770v194q0 13-8.5 21.5T770-546H576q-13 0-21.5-8.5T546-576q0-13 8.5-21.5T576-606h138q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q59 0 111-25t89-69q8-9 20.5-10t21.5 7q9 8 10 20t-7 22q-45 53-112 86.5T480-160Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.boot::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-170q-20 0-33.5-14T433-340v-286q0-21 14-34.5t33-13.5q20 0 33.5 13.5T527-626v286q0 22-14 36t-33 14Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.shutdown::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.test-note {
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-specific icons */
|
||||
{{if eq .Status "Online"}}
|
||||
.status-icon::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
}
|
||||
{{else if eq .Status "Offline"}}
|
||||
.status-icon::before {
|
||||
content: "✗";
|
||||
color: #f44336;
|
||||
}
|
||||
{{else if eq .Status "Booting"}}
|
||||
.status-icon::before {
|
||||
content: "⟳";
|
||||
color: #ffeb3b;
|
||||
display: inline-block;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
{{else if eq .Status "Shutting down"}}
|
||||
.status-icon::before {
|
||||
content: "⏻";
|
||||
color: #ff9800;
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="status-icon"></div>
|
||||
<h1 class="status-text">{{.Status}}</h1>
|
||||
<div class="server-name">Server: <strong>{{.Server}}</strong></div>
|
||||
|
||||
<div class="controls">
|
||||
<a href="/" class="button refresh">Refresh</a>
|
||||
<a href="/boot" class="button boot">Boot</a>
|
||||
<a href="/shutdown" class="button shutdown">Shutdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .IsTestMode}}
|
||||
<div class="test-panel">
|
||||
<div class="test-note">
|
||||
Running on macOS in test mode. Wake-on-LAN packets are sent, but remote shutdown is simulated.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">
|
||||
Wake-on-LAN Server Control Panel
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to load template", http.StatusInternalServerError)
|
||||
log.Printf("Template error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render template", http.StatusInternalServerError)
|
||||
log.Printf("Template render error: %v", err)
|
||||
}
|
||||
log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s",
|
||||
serverName, serverUser, macAddress, port)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Load environment variables
|
||||
loadEnvVariables()
|
||||
|
||||
// Setup template
|
||||
if err := setupTemplate(); err != nil {
|
||||
log.Fatalf("Failed to setup template: %v", err)
|
||||
}
|
||||
|
||||
// Register route handlers
|
||||
http.HandleFunc("/", indexHandler)
|
||||
http.HandleFunc("/boot", bootHandler)
|
||||
http.HandleFunc("/confirm-shutdown", confirmShutdownHandler)
|
||||
http.HandleFunc("/enter-password", enterPasswordHandler)
|
||||
http.HandleFunc("/shutdown", shutdownHandler)
|
||||
|
||||
// Start the server
|
||||
|
|
@ -408,7 +66,7 @@ func main() {
|
|||
log.Printf("Starting WOL Server on http://localhost%s", listenAddr)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
log.Println("Running on macOS in test mode - remote shutdown commands will be simulated")
|
||||
log.Println("Running on macOS - commands will be executed using the provided password")
|
||||
}
|
||||
|
||||
if err := http.ListenAndServe(listenAddr, nil); err != nil {
|
||||
|
|
|
|||
402
templates/status.html
Normal file
402
templates/status.html
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Server Status: {{.Server}}</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: {{.Color}};
|
||||
--text-color: white;
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--hover-color: rgba(255, 255, 255, 0.1);
|
||||
--card-bg: rgba(0, 0, 0, 0.15);
|
||||
--danger-color: #f44336;
|
||||
--success-color: #4caf50;
|
||||
--modal-bg: rgba(0, 0, 0, 0.85);
|
||||
--error-color: #ff6b6b;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--primary-color);
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
padding: 20px;
|
||||
transition: background-color 0.5s ease;
|
||||
background-image: radial-gradient(circle at 10% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 20%),
|
||||
radial-gradient(circle at 90% 80%, rgba(255, 255, 255, 0.05) 0%, transparent 20%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.server-name {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 15px 25px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
background-color: var(--shadow-color);
|
||||
transition: transform 0.2s ease, background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--hover-color);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 7px 14px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.button.success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.button.submit {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.button.refresh::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-160q-133 0-226.5-93.5T160-480q0-133 93.5-226.5T480-800q85 0 149 34.5T740-671v-99q0-13 8.5-21.5T770-800q13 0 21.5 8.5T800-770v194q0 13-8.5 21.5T770-546H576q-13 0-21.5-8.5T546-576q0-13 8.5-21.5T576-606h138q-38-60-97-97t-137-37q-109 0-184.5 75.5T220-480q0 109 75.5 184.5T480-220q59 0 111-25t89-69q8-9 20.5-10t21.5 7q9 8 10 20t-7 22q-45 53-112 86.5T480-160Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.boot::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-170q-20 0-33.5-14T433-340v-286q0-21 14-34.5t33-13.5q20 0 33.5 13.5T527-626v286q0 22-14 36t-33 14Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.shutdown::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M480-120q-151 0-255.5-104.5T120-480q0-138 89-239t219-120q20-3 33.5 9.5T480-797q4 20-9 35.5T437-748q-103 12-170 87t-67 181q0 124 88 212t212 88q124 0 212-88t88-212q0-109-69.5-184.5T564-748q-21-3-31.5-19T525-798q3-20 19-30.5t35-6.5q136 19 228.5 122.5T900-480q0 150-104.5 255T480-120Zm0-360Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.button.submit::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' viewBox='0 -960 960 960' width='24' fill='white'%3E%3Cpath d='M382-240 154-468l57-57 171 171 367-367 57 57-424 424Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 15px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.test-note {
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--modal-bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Form styles */
|
||||
.form-group {
|
||||
margin-bottom: 25px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--error-color);
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-actions .button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Status-specific icons */
|
||||
{{if eq .Status "Online"}}
|
||||
.status-icon::before {
|
||||
content: "✓";
|
||||
color: #4caf50;
|
||||
}
|
||||
{{else if eq .Status "Offline"}}
|
||||
.status-icon::before {
|
||||
content: "✗";
|
||||
color: #f44336;
|
||||
}
|
||||
{{else if eq .Status "Booting"}}
|
||||
.status-icon::before {
|
||||
content: "⟳";
|
||||
color: #ffeb3b;
|
||||
display: inline-block;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
{{else if eq .Status "Shutting down"}}
|
||||
.status-icon::before {
|
||||
content: "⏻";
|
||||
color: #ff9800;
|
||||
}
|
||||
{{end}}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="status-icon"></div>
|
||||
<h1 class="status-text">{{.Status}}</h1>
|
||||
<div class="server-name">Server: <strong>{{.Server}}</strong></div>
|
||||
|
||||
<div class="controls">
|
||||
<a href="/" class="button refresh">Refresh</a>
|
||||
<a href="/boot" class="button boot">Boot</a>
|
||||
<a href="/confirm-shutdown" class="button shutdown">Shutdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .IsTestMode}}
|
||||
<div class="test-panel">
|
||||
<div class="test-note">
|
||||
Running on macOS. Commands will be executed using the provided password.
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="footer">
|
||||
Wake-on-LAN Server Control Panel
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if .ConfirmShutdown}}
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Confirm Shutdown</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to shut down <strong>{{.Server}}</strong>?<br>
|
||||
This will immediately power off the server.
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<a href="/" class="button">Cancel</a>
|
||||
<a href="/enter-password" class="button danger">Yes, Continue</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .AskPassword}}
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">Enter Password</div>
|
||||
<div class="modal-body">
|
||||
Please enter the password for <strong>{{.Server}}</strong> to shutdown the server.
|
||||
|
||||
<form action="/shutdown" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="password" class="form-label">Password:</label>
|
||||
<input type="password" id="password" name="password" class="form-input" autofocus>
|
||||
</div>
|
||||
|
||||
{{if .ErrorMessage}}
|
||||
<div class="error-message">{{.ErrorMessage}}</div>
|
||||
{{end}}
|
||||
|
||||
<div class="modal-actions">
|
||||
<a href="/" class="button">Cancel</a>
|
||||
<button type="submit" class="button submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
100
utils.go
Normal file
100
utils.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// StatusData holds data for the HTML template
|
||||
type StatusData struct {
|
||||
Server string
|
||||
Status string
|
||||
Color string
|
||||
IsTestMode bool
|
||||
ConfirmShutdown bool
|
||||
AskPassword bool
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
var tmpl *template.Template
|
||||
|
||||
// Setup the HTML template
|
||||
func setupTemplate() error {
|
||||
// Check if templates directory exists, create if not
|
||||
if _, err := os.Stat("templates"); os.IsNotExist(err) {
|
||||
if err := os.Mkdir("templates", 0755); err != nil {
|
||||
return fmt.Errorf("failed to create templates directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Path to the template file
|
||||
templatePath := filepath.Join("templates", "status.html")
|
||||
|
||||
// Check if the template file exists
|
||||
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
|
||||
log.Printf("Template file not found at %s. Please create it.", templatePath)
|
||||
return fmt.Errorf("template file not found: %s", templatePath)
|
||||
}
|
||||
|
||||
// Parse the template from the file
|
||||
var err error
|
||||
tmpl, err = template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse template: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if server is online
|
||||
func isServerOnline() bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// macOS and Linux have slightly different ping commands
|
||||
if runtime.GOOS == "darwin" {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName)
|
||||
} else {
|
||||
cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName)
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Send WOL packet
|
||||
func sendWakeOnLAN() error {
|
||||
log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress)
|
||||
cmd := exec.Command("wakeonlan", macAddress)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Shutdown server with password
|
||||
func shutdownServer(password string) error {
|
||||
log.Printf("Sending shutdown command to %s", serverName)
|
||||
|
||||
// Add more SSH options to handle potential issues
|
||||
cmd := exec.Command("sshpass", "-p", password, "ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
fmt.Sprintf("%s@%s", serverUser, serverName),
|
||||
"sudo", "-S", "shutdown", "-h", "now")
|
||||
|
||||
// Capture stderr to log any error messages
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.Printf("SSH Error details: %s", stderr.String())
|
||||
return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue