Skip to content

Network Access for Spells

Spells can call web services with Http, and stay connected to one with WebSocket. As the owner of a server, you decide which hosts they reach. Until you list a host, spells reach nothing at all.

For example, to let spells ask Modrinth for the newest Wizards of Lua, list its host in config/wizards-of-lua/net.luaconf:

hosts = { "api.modrinth.com" }

Then this spell tells you when an update is out:

local response = Http:request({
  url = "https://api.modrinth.com/v2/project/wizards-of-lua/version",
  query = {
    loaders = '["' .. WizardsOfLua.loader .. '"]',
    game_versions = '["' .. spell.server.version .. '"]',
  },
})
if response and response.status == 200 then
  local newest = Json:decode(response.body)[1].version_number:match("^[%d.]+")
  local installed = WizardsOfLua.version:match("^[%d.]+")
  if newest ~= installed then
    print("Wizards of Lua " .. newest .. " is out, you have " .. installed)
  end
end

The host list

The list is in config/wizards-of-lua/net.luaconf. The file appears the first time a spell uses the network, with an empty list. It looks like this when you have filled it in:

hosts = {
  "discord.com",     -- this name, at any port
  "*.discord.gg",    -- every name below this one, but not discord.gg itself
}
Entry Allows
"example.org" This name, at any port, and nothing below it
"*.example.org" Every name below this one, at any depth, but not example.org itself
"*" Every host, including the server's own computer and its local network
"127.0.0.1", "::1" This address. 127.0.0.1 is not the same as localhost

A few things to know:

  • A change applies to the next request. You don't need to restart the server.
  • A broken file allows nothing. Spells then get the reason config_error, and the server's log says what's wrong. An entry with a scheme, a port or a path counts as broken, as it would otherwise silently allow nothing.
  • The file is Lua, but it runs on its own. It knows nothing but env("NAME"), the value of an environment variable.
  • Redirects aren't followed. A redirect reaches the spell as a normal response, so it can't lead a request to a host that isn't listed.

What the list protects

Spells can't read or change the file. So the list keeps spell code from reaching further than you want, like a spell someone downloaded, or a spell that was written for another server. Whatever a bad spell does, it stays inside your server and the hosts you listed. An update of Wizards of Lua doesn't open network access by itself either.

The list doesn't protect you from someone who can edit files on your server. And it can't stop a spell from sending anything it knows to a host that is listed.

Be careful with "*", which reaches the computer the server runs on, and your local network too. A router's admin page or a database without a password is then in reach of every spell.


Where tokens belong

Many services want a token or a password. Keep it in a Lua file of its own that returns a table, in config/wizards-of-lua/, and let the spell require it:

-- config/wizards-of-lua/my-service-settings.lua
return {
  token = "...",
}
local settings = require("my-service-settings")
  • Don't put it into the examples folder. /wol extract examples overwrite replaces that folder.
  • Don't write it into a command, and don't paste it into the chat: commands end up in logs.
  • Keep the file out of git, and out of backups and worlds that you hand on.

Wizards of Lua leaves tokens out of what it writes itself: its error messages name a host at most, never a URL or the value of a header.

That protects a token from accidents, not from the people who run spells: every spell can require that file. Whoever can use /lua can read the token. See who you can trust with /lua.


Limits

  • A request has 10 seconds by default, and the spell can change that.
  • A response or a message may have up to 16 MB.
  • Sockets close when their spell ends. A spell waiting in Http:request keeps its events, which wait in its queue.

NeoForge and IPv6

Most servers never notice this. It matters in two cases, and only on NeoForge.

A program on the server's own computer

Say you run a small program next to your Minecraft server, at port 8080. Use the address 127.0.0.1 for it, not localhost, in the host list and in the spell:

-- config/wizards-of-lua/net.luaconf
hosts = { "127.0.0.1" }
local response = Http:request({ url = "http://127.0.0.1:8080/status" })

On NeoForge, localhost can mean the IPv6 address ::1, and a program that only listens on 127.0.0.1 isn't found there.

A web service that fails only on NeoForge

This spell shows what happens when it calls the service:

local response, reason = Http:request({ url = "https://api.example.org/" })
print(response and response.status or reason)

If it prints connect_failed, although the service works on a Fabric server or in your browser, then the service has an IPv6 address that your server can't reach. NeoForge tells Java to try the addresses in the order your system gives, which can put IPv6 first, and Java tries only that one. To make Java try IPv4 first:

  1. Stop the server.
  2. In your server folder, open libraries/net/neoforged/neoforge/<version>/, where <version> is your NeoForge version, like 26.3.0.83.
  3. In win_args.txt (on Windows) or unix_args.txt (on Linux and macOS), change the line -Djava.net.preferIPv6Addresses=system into -Djava.net.preferIPv6Addresses=false.
  4. Start the server.

An update of NeoForge brings new files, so do this again after each update. The line doesn't work in user_jvm_args.txt or in your start script: NeoForge's own line comes later, and the later one wins. If your server runs at a hosting company and you can't open these files, ask their support to change that line for you.


An example

The Discord bridge connects the chat of your server with a Discord channel, in both directions. Its guide also shows how to create a webhook and a Discord bot.