Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/settings.d/wol.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
# Can be true, false, or http/https to enable just one of the protocols
:enabled: false

# Wake-on-LAN configuration
# The module sends standard WoL magic packets via UDP broadcast on port 9
# No additional configuration is required as it uses the standard protocol
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ module Proxy
require 'logs/logs'
require 'httpboot/httpboot'
require 'registration/registration'
require 'wol/wol'

def self.version
{:version => VERSION}
Expand Down
5 changes: 5 additions & 0 deletions modules/wol/http_config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'wol/wol_api'

map "/wol" do
run Proxy::WolApi
end
1 change: 1 addition & 0 deletions modules/wol/wol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'wol/wol_plugin'
41 changes: 41 additions & 0 deletions modules/wol/wol_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'proxy/validations'
require 'wol/wol_packet_sender'

class Proxy::WolApi < Sinatra::Base
include Proxy::Validations
helpers ::Proxy::Helpers
authorize_with_trusted_hosts
authorize_with_ssl_client

post "/" do
content_type :json

# Parse JSON body and merge with URL parameters
body_params = parse_json_body
all_params = params.merge(body_params)

# Get MAC address from either URL params or JSON body
mac_address = all_params[:mac_address] || all_params['mac_address']

logger.debug "WoL API - Final MAC address: #{mac_address}"

begin
mac_address = validate_mac(mac_address)
rescue Proxy::Validations::InvalidMACAddress => e
log_halt 400, "Invalid MAC address provided: #{e.message}"
end

# Send Wake-on-LAN magic packet
begin
Proxy::Wol::WolPacketSender.send_magic_packet(mac_address)

# Log the attempt
logger.info "Wake-on-LAN packet sent to MAC address: #{mac_address}"

{ :status => "success", :message => "Wake-on-LAN packet sent successfully", :mac_address => mac_address }.to_json
rescue => e
logger.error "Failed to send Wake-on-LAN packet to #{mac_address}: #{e.message}"
log_halt 500, "Failed to send Wake-on-LAN packet: #{e.message}"
end
end
end
31 changes: 31 additions & 0 deletions modules/wol/wol_packet_sender.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'socket'

module Proxy
module Wol
class WolPacketSender
# Sends a Wake-on-LAN magic packet to the specified MAC address
def self.send_magic_packet(mac_address)
# Create magic packet using the existing method
packet = create_magic_packet(mac_address)

# Send UDP broadcast on port 9 (WoL standard port)
socket = UDPSocket.new
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
socket.send(packet, 0, '255.255.255.255', 9)
socket.close
end

# Creates a magic packet for the given MAC address (useful for testing)
def self.create_magic_packet(mac_address)
# Clean up MAC address and convert to binary
mac_bytes = mac_address.gsub(/[:-]/, '').scan(/../).map { |hex| hex.to_i(16) }

# Create magic packet: 6 bytes of 0xFF followed by 16 repetitions of MAC address
magic_packet = [0xFF] * 6 + mac_bytes * 16

# Convert to binary string
magic_packet.pack('C*')
end
end
end
end
10 changes: 10 additions & 0 deletions modules/wol/wol_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class ::Proxy::WolPlugin < ::Proxy::Plugin
rackup_path File.expand_path("http_config.ru", __dir__)

plugin :wol, ::Proxy::VERSION
default_settings :enabled => true

after_activation do
logger.debug "Wake-on-LAN plugin initialized"
end
end
251 changes: 251 additions & 0 deletions test/wol/wol_api_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
require File.join(__dir__, '..', 'test_helper')
require 'json'
require 'wol/wol_api'

ENV['RACK_ENV'] = 'test'

class WolApiTest < Test::Unit::TestCase
include Rack::Test::Methods

def app
Proxy::WolApi.new
end

def setup
# Mock UDPSocket to prevent actual network packets during tests
@mock_socket = mock('UDPSocket')
UDPSocket.stubs(:new).returns(@mock_socket)
@mock_socket.stubs(:setsockopt)
@mock_socket.stubs(:send)
@mock_socket.stubs(:close)

# By default, stub the packet sender to prevent actual network calls
# Individual tests can override this if they need to test socket operations
stub_packet_sender
end

def test_valid_mac_address_with_colons
mac = "54:ee:75:87:1f:fb"

post "/", :mac_address => mac

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
assert_equal "Wake-on-LAN packet sent successfully", data["message"]
assert_equal mac, data["mac_address"]
end

def test_valid_mac_address_uppercase
mac = "AA:BB:CC:DD:EE:FF"

post "/", :mac_address => mac

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
# The validation system normalizes MAC addresses to lowercase
assert_equal mac.downcase, data["mac_address"]
end

def test_valid_mac_address_lowercase
mac = "aa:bb:cc:dd:ee:ff"

post "/", :mac_address => mac

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
assert_equal mac, data["mac_address"]
end

def test_valid_mac_address_mixed_case
mac = "Ab:Cd:Ef:12:34:56"

post "/", :mac_address => mac

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
# The validation system normalizes MAC addresses to lowercase
assert_equal mac.downcase, data["mac_address"]
end

def test_json_request_with_valid_mac
mac = "54:ee:75:87:1f:fb"

post "/", { mac_address: mac }.to_json, "CONTENT_TYPE" => "application/json"

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
# MAC addresses are normalized to lowercase
assert_equal mac.downcase, data["mac_address"]
end

def test_json_request_with_charset
mac = "54:ee:75:87:1f:fb"

post "/", { mac_address: mac }.to_json, "CONTENT_TYPE" => "application/json; charset=utf-8"

assert last_response.ok?, "Last response was not ok: #{last_response.status} #{last_response.body}"
data = JSON.parse(last_response.body)
assert_equal "success", data["status"]
# MAC addresses are normalized to lowercase
assert_equal mac.downcase, data["mac_address"]
end

def test_missing_mac_address
post "/"

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_empty_mac_address
post "/", :mac_address => ""

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_nil_mac_address
post "/", :mac_address => nil

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_invalid_mac_address_too_short
post "/", :mac_address => "54:ee:75:87:1f"

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_invalid_mac_address_too_long
post "/", :mac_address => "54:ee:75:87:1f:fb:00"

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_invalid_mac_address_invalid_characters
post "/", :mac_address => "54:ee:75:87:1g:fb"

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_invalid_mac_address_wrong_format
post "/", :mac_address => "54ee75871ffb"

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

def test_invalid_json_body
post "/", "{ invalid json", "CONTENT_TYPE" => "application/json"

assert_equal 415, last_response.status
assert_match(/Invalid JSON content in body of request/, last_response.body)
end

def test_json_with_invalid_data_type
post "/", "\"not a hash\"", "CONTENT_TYPE" => "application/json"

assert_equal 415, last_response.status
assert_match(/Invalid JSON content in body of request/, last_response.body)
end

def test_empty_json_body
post "/", "", "CONTENT_TYPE" => "application/json"

assert_equal 415, last_response.status
assert_match(/Invalid JSON content in body of request/, last_response.body)
end

def test_socket_creation_error
unstub_packet_sender
Proxy::Wol::WolPacketSender.stubs(:send_magic_packet).raises(StandardError.new("Socket creation failed"))

post "/", :mac_address => "54:ee:75:87:1f:fb"

assert_equal 500, last_response.status
assert_match(/Failed to send Wake-on-LAN packet/, last_response.body)
end

def test_socket_send_error
unstub_packet_sender
Proxy::Wol::WolPacketSender.stubs(:send_magic_packet).raises(StandardError.new("Network unreachable"))

post "/", :mac_address => "54:ee:75:87:1f:fb"

assert_equal 500, last_response.status
assert_match(/Failed to send Wake-on-LAN packet/, last_response.body)
end

def test_response_content_type_is_json
post "/", :mac_address => "54:ee:75:87:1f:fb"

assert last_response.ok?
assert_match(%r{application/json}, last_response.content_type)
end

def test_magic_packet_creation
unstub_packet_sender

mac = "54:ee:75:87:1f:fb"
expected_mac_bytes = [0x54, 0xee, 0x75, 0x87, 0x1f, 0xfb]
expected_magic_packet = [0xFF] * 6 + expected_mac_bytes * 16
expected_packet = expected_magic_packet.pack('C*')

@mock_socket.expects(:send).with(expected_packet, 0, '255.255.255.255', 9)

post "/", :mac_address => mac

assert last_response.ok?
end

def test_socket_broadcast_option
unstub_packet_sender

@mock_socket.expects(:setsockopt).with(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)

post "/", :mac_address => "54:ee:75:87:1f:fb"

assert last_response.ok?
end

def test_socket_close_called
unstub_packet_sender

@mock_socket.expects(:close)

post "/", :mac_address => "54:ee:75:87:1f:fb"

assert last_response.ok?
end

def test_mac_address_with_whitespace
mac = " 54:ee:75:87:1f:fb "

# Since the current implementation doesn't strip whitespace,
# this should fail. If whitespace handling is added later,
# this test should be updated to expect success.
post "/", :mac_address => mac

assert_equal 400, last_response.status
assert_match(/Invalid MAC address provided/, last_response.body)
end

private

def stub_packet_sender
Proxy::Wol::WolPacketSender.stubs(:send_magic_packet)
end

def unstub_packet_sender
Proxy::Wol::WolPacketSender.unstub(:send_magic_packet)
end
end
Loading