Initialize Go module

This commit is contained in:
Lorenzo Iovino 2025-04-21 22:21:02 +02:00
parent f9549a10bb
commit 64753ed498
10 changed files with 1101 additions and 537 deletions

4
.env Normal file
View 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
View file

@ -0,0 +1,4 @@
SERVER_NAME=pippo
SERVER_USER=root
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
PORT=8080

View file

@ -43,63 +43,152 @@ jobs:
- name: Create deployment script - name: Create deployment script
run: | run: |
cat > deploy.sh << 'EOL' cat > install.sh << 'EOL'
#!/bin/bash #!/bin/bash
set -e
# Detect Raspberry Pi model and use the appropriate binary # Installation directory
MODEL=$(cat /proc/cpuinfo | grep "Model" | sed 's/Model\s*:\s*//g') INSTALL_DIR=~/wol-server
echo "Detected model: $MODEL"
# Select the right binary based on processor architecture echo "Creating installation directory..."
ARCH=$(uname -m) mkdir -p $INSTALL_DIR
echo "Architecture: $ARCH" mkdir -p $INSTALL_DIR/templates
if [[ "$ARCH" == "aarch64" ]]; then echo "Installing application..."
echo "Using ARM64 binary" cp wol-server-arm6 $INSTALL_DIR/wol-server
cp wol-server-arm64 wol-server chmod +x $INSTALL_DIR/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 "Creating directory..." echo "Installing template files..."
mkdir -p ~/wol-server cp -r templates/* $INSTALL_DIR/templates/
echo "Copying application..." echo "Installing system service..."
cp wol-server ~/wol-server/
chmod +x ~/wol-server/wol-server
echo "Installing service..."
sudo cp wol-server.service /etc/systemd/system/ sudo cp wol-server.service /etc/systemd/system/
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable wol-server 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 sudo systemctl restart wol-server
echo "Deployment complete!" echo "==========================================="
echo "Service status:" echo "Installation complete!"
sudo systemctl status wol-server echo "The WOL server is now running at http://$(hostname -I | awk '{print $1}'):8080"
echo "==========================================="
EOL EOL
chmod +x deploy.sh chmod +x install.sh
- name: Create archive for each platform - name: Create README with installation instructions
run: | run: |
# ARMv6 package cat > INSTALL.md << 'EOL'
mkdir -p armv6-package # WOL Server Installation Guide
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 .
# All-in-one package This guide will help you install the Wake-on-LAN server on your Raspberry Pi.
mkdir -p all-package
cp wol-server-arm6 all-package/ ## Prerequisites
cp wol-server.service all-package/
cp deploy.sh all-package/ - Raspberry Pi running Raspberry Pi OS (Raspbian)
tar -czf wol-server-all.tar.gz -C all-package . - 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 - name: Create Release
id: create_release id: create_release
@ -111,10 +200,7 @@ jobs:
draft: false draft: false
prerelease: false prerelease: false
files: | files: |
wol-server-arm6 wol-server.tar.gz
wol-server.service INSTALL.md
deploy.sh
wol-server-armv6.tar.gz
wol-server-all.tar.gz
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

315
README.md
View file

@ -1,147 +1,234 @@
![Server Status Screenshot](https://placeholder-for-screenshot.png) # 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.
![WOL Server Screenshot](https://i.imgur.com/example.jpg)
## Features ## Features
- **Status Monitoring**: Real-time status checks to determine if your server is online or offline - **Simple Web Interface**: Boot and shut down your server with a clean, responsive UI
- **Wake-on-LAN**: Boot your server remotely with the click of a button - **Status Monitoring**: Check if your target device is online
- **Remote Shutdown**: Safely shut down your server when it's not needed - **Raspberry Pi Optimized**: Built specifically for ARM processors found in all Raspberry Pi models
- **Responsive UI**: Simple, mobile-friendly interface with color-coded status indicators - **Secure Shutdown**: Password-protected shutdown functionality
- **Lightweight**: Built with Go for minimal resource usage, perfect for Raspberry Pi Zero - **Lightweight**: Minimal resource usage ideal for running on even the oldest Pi models
- **Easy Setup**: Simple installation process with clear instructions
## 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
## Installation ## Installation
### 1. Install Dependencies ### Prerequisites
```bash - Raspberry Pi (any model) running Raspberry Pi OS
sudo apt update - Network connection
sudo apt install golang-go wakeonlan - Basic knowledge of SSH/terminal
```
### 2. Clone the Repository ### Option 1: One-Command Installation
```bash 1. **Download the latest release** on your local machine from the [Releases page](https://github.com/yourusername/wol-server/releases)
git clone https://github.com/yourusername/wol-server.git
cd wol-server
```
### 3. Configure the Application 2. **Transfer the package to your Raspberry Pi** using SCP:
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
```bash
go build -o wol-server
```
### 5. Set Up as a System Service
Create a systemd service file:
```bash
sudo nano /etc/systemd/system/wol-server.service
```
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:
```bash
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:
```bash ```bash
ssh-keygen -t rsa scp wol-server.tar.gz pi@your-pi-ip:~/
``` ```
2. Copy the key to your target server: 3. **SSH into your Raspberry Pi**:
```bash ```bash
ssh-copy-id user@yourserver ssh pi@your-pi-ip
``` ```
3. Configure sudo on the target server to allow password-less shutdown: 4. **Install with a single command**:
```bash ```bash
# On the target server, run: tar -xzf wol-server.tar.gz && ./install.sh
sudo visudo
# Add this line (replacing 'user' with your username):
user ALL=(ALL) NOPASSWD: /sbin/shutdown
``` ```
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
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
```
4. **Install required dependencies**:
```bash
sudo apt-get update
sudo apt-get install -y wakeonlan sshpass
```
## Configuration
The application can be configured by editing the `.env` file in the installation directory:
```bash
nano ~/wol-server/.env
```
### 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 |
After changing configuration, restart the service:
```bash
sudo systemctl restart wol-server
```
## Usage ## 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) ### Features
- Boot button - sends a Wake-on-LAN magic packet to your server
- Shutdown button - safely shuts down your server via SSH - **Status Checking**: The interface shows the current status (Online/Offline)
- Refresh button - manually updates the status display - **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 ## Troubleshooting
- **Server won't boot**: ### Service Won't Start
- 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
- **Shutdown doesn't work**: Check for template errors:
- Verify SSH key setup ```bash
- Check sudo configuration on target server ls -la ~/wol-server/templates/
- Test manual SSH command: `ssh user@server sudo shutdown -h now` ```
- **Web interface not accessible**: Verify the .env file exists:
- Ensure the service is running: `sudo systemctl status wol-server` ```bash
- Check for firewall rules blocking port 8080 cat ~/wol-server/.env
- Verify the Raspberry Pi is connected to the network ```
## 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. 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
View 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
View 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
View 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
View file

@ -2,405 +2,63 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"log" "log"
"net/http" "net/http"
"os/exec" "os"
"runtime" "runtime"
"github.com/joho/godotenv"
) )
const ( // Default values
serverName = "delemaco" // Server to ping var (
macAddress = "b8:cb:29:a1:f3:88" // MAC address of the server 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 port = "8080" // Port to listen on
) )
// StatusData holds data for the HTML template func loadEnvVariables() {
type StatusData struct { // Load .env file if it exists
Server string if err := godotenv.Load(); err != nil {
Status string log.Println("No .env file found, using default values")
Color string
IsTestMode bool
}
// 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() // Override defaults with environment variables if they exist
return err == nil if envServerName := os.Getenv("SERVER_NAME"); envServerName != "" {
} serverName = envServerName
// 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
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()
}
// 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() if envServerUser := os.Getenv("SERVER_USER"); envServerUser != "" {
status := "Online" serverUser = envServerUser
color := "#4caf50" // Material green
if !online {
status = "Offline"
color = "#d32f2f" // Material red
} }
data := StatusData{ if envMacAddress := os.Getenv("MAC_ADDRESS"); envMacAddress != "" {
Server: serverName, macAddress = envMacAddress
Status: status,
Color: color,
IsTestMode: runtime.GOOS == "darwin",
} }
renderTemplate(w, data) if envPort := os.Getenv("PORT"); envPort != "" {
} port = envPort
// 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 log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s",
data := StatusData{ serverName, serverUser, macAddress, port)
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)
}
} }
func main() { func main() {
// Load environment variables
loadEnvVariables()
// Setup template
if err := setupTemplate(); err != nil {
log.Fatalf("Failed to setup template: %v", err)
}
// Register route handlers // Register route handlers
http.HandleFunc("/", indexHandler) http.HandleFunc("/", indexHandler)
http.HandleFunc("/boot", bootHandler) http.HandleFunc("/boot", bootHandler)
http.HandleFunc("/confirm-shutdown", confirmShutdownHandler)
http.HandleFunc("/enter-password", enterPasswordHandler)
http.HandleFunc("/shutdown", shutdownHandler) http.HandleFunc("/shutdown", shutdownHandler)
// Start the server // Start the server
@ -408,7 +66,7 @@ func main() {
log.Printf("Starting WOL Server on http://localhost%s", listenAddr) log.Printf("Starting WOL Server on http://localhost%s", listenAddr)
if runtime.GOOS == "darwin" { 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 { if err := http.ListenAndServe(listenAddr, nil); err != nil {

402
templates/status.html Normal file
View 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
View 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
}