Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f3ba16b6 |
9 changed files with 1004 additions and 467 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
|
||||||
326
README.md
326
README.md
|
|
@ -1,66 +1,105 @@
|
||||||

|
# WOL-Server
|
||||||
|
|
||||||
|
A lightweight Wake-on-LAN (WOL) server application designed specifically for Raspberry Pi devices. This tool allows you to remotely power on your network devices using magic packets through a simple, user-friendly interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Status Monitoring**: Real-time status checks to determine if your server is online or offline
|
- **Cross-Platform Compatibility**: Optimized for various Raspberry Pi models (Zero, 1, 2, 3, 4)
|
||||||
- **Wake-on-LAN**: Boot your server remotely with the click of a button
|
- **Easy Installation**: Automated deployment script for quick setup
|
||||||
- **Remote Shutdown**: Safely shut down your server when it's not needed
|
- **Systemd Integration**: Runs as a system service for reliability
|
||||||
- **Responsive UI**: Simple, mobile-friendly interface with color-coded status indicators
|
- **Simple Web Interface**: Control your devices through an intuitive web UI
|
||||||
- **Lightweight**: Built with Go for minimal resource usage, perfect for Raspberry Pi Zero
|
- **Configurable**: Easily customize settings through environment variables or `.env` file
|
||||||
|
- **Low Resource Usage**: Minimal footprint to run efficiently on any Pi
|
||||||
## 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
|
### Method 1: Pre-built Release (Recommended)
|
||||||
|
|
||||||
|
1. **Check which binary is right for your Raspberry Pi model**:
|
||||||
|
```bash
|
||||||
|
uname -m
|
||||||
|
```
|
||||||
|
- `armv6l`: Use `wol-server-arm6` (Pi Zero, Pi 1)
|
||||||
|
- `armv7l`: Use `wol-server-arm7` (Pi 2, Pi 3 32-bit)
|
||||||
|
- `aarch64`: Use `wol-server-arm64` (Pi 3/4 64-bit)
|
||||||
|
|
||||||
|
2. **Download the latest release**:
|
||||||
|
|
||||||
|
Navigate to [Releases](https://github.com/thisloke/wol-server/releases) and download the appropriate files for your Pi, or use the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a directory for installation
|
||||||
|
mkdir -p ~/wol-install
|
||||||
|
cd ~/wol-install
|
||||||
|
|
||||||
|
# Download the executable (replace v6 with the latest version number)
|
||||||
|
wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server-arm6
|
||||||
|
|
||||||
|
# Download the service file
|
||||||
|
wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server.service
|
||||||
|
|
||||||
|
# Download the deploy script
|
||||||
|
wget https://github.com/thisloke/wol-server/releases/download/v6/deploy.sh
|
||||||
|
|
||||||
|
# Make files executable
|
||||||
|
chmod +x wol-server-arm6
|
||||||
|
chmod +x deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create a `.env` file for configuration**:
|
||||||
|
```bash
|
||||||
|
cat > .env << EOL
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_NAME=pippo
|
||||||
|
SERVER_USER=root
|
||||||
|
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
||||||
|
PORT=8080
|
||||||
|
EOL
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run the deployment script**:
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Build from Source
|
||||||
|
|
||||||
|
If you prefer to build the application from source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Install Go (if not already installed)
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install golang-go wakeonlan
|
sudo apt install golang-go
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Clone the Repository
|
# Clone the repository
|
||||||
|
git clone https://github.com/thisloke/wol-server.git
|
||||||
```bash
|
|
||||||
git clone https://github.com/yourusername/wol-server.git
|
|
||||||
cd wol-server
|
cd wol-server
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Configure the Application
|
# Install dependencies
|
||||||
|
go get github.com/joho/godotenv
|
||||||
|
|
||||||
Edit the constants in `main.go` to match your server:
|
# Create a .env file for configuration
|
||||||
|
cat > .env << EOL
|
||||||
|
# Server Configuration
|
||||||
|
SERVER_NAME=pippo
|
||||||
|
SERVER_USER=root
|
||||||
|
MAC_ADDRESS=aa:aa:aa:aa:aa:aa
|
||||||
|
PORT=8080
|
||||||
|
EOL
|
||||||
|
|
||||||
```go
|
# Build the application
|
||||||
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
|
go build -o wol-server
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Set Up as a System Service
|
# Create installation directory
|
||||||
|
mkdir -p ~/wol-server
|
||||||
|
|
||||||
Create a systemd service file:
|
# Copy the binary and config
|
||||||
|
cp wol-server ~/wol-server/
|
||||||
|
cp .env ~/wol-server/
|
||||||
|
chmod +x ~/wol-server/wol-server
|
||||||
|
|
||||||
```bash
|
# Create and install systemd service
|
||||||
sudo nano /etc/systemd/system/wol-server.service
|
sudo bash -c 'cat > /etc/systemd/system/wol-server.service << EOL
|
||||||
```
|
|
||||||
|
|
||||||
Add the following content (adjust paths if needed):
|
|
||||||
|
|
||||||
```
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=WOL Server Go Application
|
Description=WOL Server Go Application
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
@ -73,75 +112,182 @@ Restart=always
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
EOL'
|
||||||
|
|
||||||
Enable and start the service:
|
# Enable and start the service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
```bash
|
|
||||||
sudo systemctl enable wol-server
|
sudo systemctl enable wol-server
|
||||||
sudo systemctl start wol-server
|
sudo systemctl start wol-server
|
||||||
```
|
```
|
||||||
|
|
||||||
## SSH Configuration for Remote Shutdown
|
### Method 3: Manual Installation
|
||||||
|
|
||||||
For the shutdown functionality to work, you need to set up password-less SSH:
|
If you encounter issues with the deployment script:
|
||||||
|
|
||||||
1. Generate an SSH key on your Raspberry Pi:
|
```bash
|
||||||
```bash
|
# Create the application directory
|
||||||
ssh-keygen -t rsa
|
mkdir -p ~/wol-server
|
||||||
```
|
|
||||||
|
|
||||||
2. Copy the key to your target server:
|
# Copy the binary and rename it
|
||||||
```bash
|
cp wol-server-arm6 ~/wol-server/wol-server
|
||||||
ssh-copy-id user@yourserver
|
chmod +x ~/wol-server/wol-server
|
||||||
```
|
|
||||||
|
|
||||||
3. Configure sudo on the target server to allow password-less shutdown:
|
# Copy the .env file
|
||||||
```bash
|
cp .env ~/wol-server/
|
||||||
# On the target server, run:
|
|
||||||
sudo visudo
|
|
||||||
|
|
||||||
# Add this line (replacing 'user' with your username):
|
# Install the service file
|
||||||
user ALL=(ALL) NOPASSWD: /sbin/shutdown
|
sudo cp wol-server.service /etc/systemd/system/
|
||||||
```
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable wol-server
|
||||||
|
sudo systemctl start wol-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
WOL-server can be configured using environment variables or a `.env` file in the application directory.
|
||||||
|
|
||||||
|
### Available Configuration Options
|
||||||
|
|
||||||
|
| Environment Variable | Description | Default Value |
|
||||||
|
|----------------------|-------------|---------------|
|
||||||
|
| `SERVER_NAME` | Name of the server to ping/wake | `pippo` |
|
||||||
|
| `SERVER_USER` | SSH username for remote commands | `root` |
|
||||||
|
| `MAC_ADDRESS` | MAC address of the target server | `aa:aa:aa:aa:aa:aa` |
|
||||||
|
| `PORT` | The port number for the web server | `8080` |
|
||||||
|
|
||||||
|
### Customizing Your Configuration
|
||||||
|
|
||||||
|
You can edit the `.env` file to modify the application's behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to the installation directory
|
||||||
|
cd ~/wol-server
|
||||||
|
|
||||||
|
# Edit the .env file
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
After modifying the configuration, restart the service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart wol-server
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Access the web interface by navigating to `http://raspberry-pi-ip:8080` in your browser.
|
Once installed, the WOL server will be accessible at:
|
||||||
|
```
|
||||||
|
http://your-pi-ip:8080
|
||||||
|
```
|
||||||
|
(or whatever port you've configured)
|
||||||
|
|
||||||
The interface provides:
|
### Using the Web Interface
|
||||||
|
|
||||||
- Current server status (Online/Offline)
|
1. **Wake Server**: Click the "Boot" button to send a Wake-on-LAN magic packet to the configured MAC address
|
||||||
- Boot button - sends a Wake-on-LAN magic packet to your server
|
2. **Shut Down Server**: Click the "Shutdown" button, confirm, and enter your password if required
|
||||||
- Shutdown button - safely shuts down your server via SSH
|
|
||||||
- Refresh button - manually updates the status display
|
## Service Management
|
||||||
|
|
||||||
|
Control the WOL server using standard systemd commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check service status
|
||||||
|
sudo systemctl status wol-server
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
sudo systemctl stop wol-server
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo systemctl start wol-server
|
||||||
|
|
||||||
|
# Restart the service
|
||||||
|
sudo systemctl restart wol-server
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
journalctl -u wol-server -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Updating to a New Version
|
||||||
|
|
||||||
|
To update to a new version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to a temporary directory
|
||||||
|
mkdir -p ~/wol-update
|
||||||
|
cd ~/wol-update
|
||||||
|
|
||||||
|
# Download the latest release (replace v7 with the latest version number)
|
||||||
|
wget https://github.com/thisloke/wol-server/releases/download/v7/wol-server-arm6
|
||||||
|
chmod +x wol-server-arm6
|
||||||
|
|
||||||
|
# Stop the service
|
||||||
|
sudo systemctl stop wol-server
|
||||||
|
|
||||||
|
# Replace the binary
|
||||||
|
cp wol-server-arm6 ~/wol-server/wol-server
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
sudo systemctl start wol-server
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cd ~
|
||||||
|
rm -rf ~/wol-update
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
- **Server won't boot**:
|
### Checking Your Raspberry Pi Architecture
|
||||||
- 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**:
|
If you need to verify which version of the application you should use:
|
||||||
- Verify SSH key setup
|
|
||||||
- Check sudo configuration on target server
|
|
||||||
- Test manual SSH command: `ssh user@server sudo shutdown -h now`
|
|
||||||
|
|
||||||
- **Web interface not accessible**:
|
```bash
|
||||||
- Ensure the service is running: `sudo systemctl status wol-server`
|
uname -m
|
||||||
- Check for firewall rules blocking port 8080
|
```
|
||||||
- Verify the Raspberry Pi is connected to the network
|
|
||||||
|
This will output your Pi's architecture:
|
||||||
|
- `armv6l`: Use the `wol-server-arm6` binary (Pi Zero, Pi 1)
|
||||||
|
- `armv7l`: Use the `wol-server-arm7` binary (Pi 2, Pi 3)
|
||||||
|
- `aarch64`: Use the `wol-server-arm64` binary (64-bit Pi 3, Pi 4)
|
||||||
|
|
||||||
|
### Service Not Starting
|
||||||
|
|
||||||
|
If the service doesn't start properly, check the logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u wol-server -e
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Configuration
|
||||||
|
|
||||||
|
Verify that your configuration is being properly loaded:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View the environment variables being used
|
||||||
|
sudo systemctl status wol-server
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for a line in the output that shows the loaded configuration values.
|
||||||
|
|
||||||
|
### Permission Issues
|
||||||
|
|
||||||
|
Make sure the binary has execute permissions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/wol-server/wol-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Download Release Files
|
||||||
|
|
||||||
|
If you're having trouble downloading the release files directly, you can also:
|
||||||
|
|
||||||
|
1. Download the files on your computer
|
||||||
|
2. Transfer them to your Raspberry Pi using SCP, SFTP, or a USB drive
|
||||||
|
3. Continue with the installation steps as described above
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
Contributions are welcome! Feel free to submit pull requests or open issues for bugs and feature requests.
|
||||||
|
|
||||||
## License
|
## 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
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 (
|
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
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