Domain Lock HTML5 games

This is a basic module to help you lock HTML5 games to a specific domain (or a list of domains). This can be required for licensing for example, to keep other sites from hosting your games without your permission that easily. Most web browsers do not allow spoofing location.hostname for security reasons so it’s pretty safe to use for checking current domain.

You’ll want to provide a helpful message in case of domain check failure. When not running on HTML5 builds the default behavior is to always pass a domain check.

You will want to add every version of your domain if your games can be hosted on them. Such as www.yougamesdomain.com and yougamesdmain.com both. You’ll probably want to add “localhost” while debugging. You can use sys.get_engine_info to check if the engine is running in debug or not.

If you make improvements please post them here! For example, allowing wildcard for subdomains as an option to enable would be nice.

local domainlock = require("utils.domainlock")
domainlock.add_domain("localhost")
domainlock.add_domain("defold.com")
domainlock.add_domain("www.defold.com")
if domainlock.verify_domain() then print("hurray! we're on a verified domain!") end

domainlock.lua (457 Bytes)

local M = {}

M.domains = {}

function M.add_domain(domain)
	table.insert(M.domains, domain)
end

function M.verify_domain()
	if not html5 then return true end
	local current_domain = html5.run("location.hostname")
	for key, value in ipairs(M.domains) do
		if value == current_domain then
			return true
		end
	end
	return false
end

function M.get_current_domain()
	if html5 then
		return html5.run("location.hostname")
	else
		return ""
	end
end

return M
17 Likes

Does this still work? I’ve faced a weird behaviour after updating Editor to 1.2.157 (and maybe even to 1.2.156) where string.find() stopped working as expected.

Try this:
local domain = "anvil-games.com"
print (string.find(domain, domain))
It prints nil for me, what means, it can’t find the string in itself. It worked before and I can’t understand why it stopped working.

BTW this string.find(domain, domain, 1, true) works. Docs

Any ideas?

string.find searches for patterns, not strings (read the chapter on patterns from Programming In Lua). The minus symbol in a pattern means zero or more repetitions of the previous character. Passing true as the fourth argument interprets the second arg as a plain string, not a pattern, which is what you want.

See https://www.lua.org/manual/5.1/manual.html#5.4

2 Likes

string.find() isn’t used in this module? The source is posted above, it just checks against location.hostname

Subdomain matters. So if you want www.anvil-games.com to work you need to add that variant too.

2 Likes

Oops, sorry, my bad, I’ve made a little change to your code to search for substring instead of precisely comparing strings. I need this because for one of publishers there is generated subdomain in the game url like „randomname12345.yandex.net” and I cannot hardcode it, but „yandex.net” part is always the same, so I,ve used string.find()

1 Like

You should have posted your changes! :smiley:

I’m not sure of best way to support wildcard domains in the module.

Maybe if a star is detected at the start of the domain like “*. domain.com” then compare the test domain against the test domain and a version with a split off the first subdomain. You don’t always want wildcard domain behavior enabled for everyone by default though because some common cdn hosting platforms are used by some people.

1 Like

Here’s a test, but there is probably a more elegant way to do it…

local M = {}

M.domains = {}

local function split(s, delimiter)
	delimiter = delimiter or '%s'
	local t={}
	local i=1
	for str in string.gmatch(s, '([^'..delimiter..']+)') do
		t[i] = str
		i = i + 1
	end
	return t
end

local function join(t, delimiter)
	delimiter = delimiter or ' '
	return table.concat(t, delimiter)	
end

function M.add_domain(domain)
	table.insert(M.domains, domain)
end

function M.verify_domain()
	if not html5 then return true end
	local current_domain = html5.run("location.hostname")
	for key, value in ipairs(M.domains) do
    -- check if added domain is wildcard
    local first = string.sub(value, 1, 1)
    if first == "*" then
      local actual_domain = string.sub(value, 3, #value)
      local actual_current_domain = split(current_domain, ".")
      table.remove(actual_current_domain, 1)
      actual_current_domain = join(actual_current_domain, ".")
      if actual_domain == actual_current_domain or actual_domain == current_domain then
        return true
      end
    else
      if value == current_domain then
        return true
      end    
    end
	end
	return false
end

function M.get_current_domain()
	if html5 then
		return html5.run("location.hostname")
	else
		return ""
	end
end

By the way, not like this is a problem you would ever have someone figure out, but the string.find method would be vulnerable to sub domain based “attack” hosting so if you had randomname12345.yandex.net as your lock domain someone could host the game on randomname12345.yandex.net.evildomain.com and it would work, I think.

1 Like

Lua pattern matching should be powerful enough to solve most kinds of comparisons needed, including a random portion in a domain.

1 Like

yes, I thought about it too, possible solution - break domain by “.” symbol and check from end, like:

but i’m not sure if I’ll implement this, the whole check was just as a foolproof from very basic “hack”

UPD: or just simply check if substring with the length of the pattern exists in the end of domain you’re checking.

1 Like

The modified version I posted above is that more of less. So with it you would add “*.yandex.net” as a wildcard domain and it would work with any subdomain but not on any other domain not added.

@britzl How would you set the wildcard domain “*.example.com” still?

2 Likes

I’m thinking something like this:

local function verify(domain, accepted_domains)
	for _,accepted in ipairs(accepted_domains) do
		if string.find(domain, accepted) then
			return true
		end
	end
	return false
end


local function test(domain, domains)
	print(domain, verify(domain, domains))
end

local domains = {
	"^localhost$",
	".*%.defold%.com$",
}

test("www.defold.com", domains)
test("foo.defold.com", domains)
test("foo.defold.com.evildomain.com", domains)
test("localhost", domains)
test("localhosty", domains)

Some results:

$ lua domainlock.lua 
www.defold.com true
foo.defold.com true
foo.defold.com.evildomain.com false
localhost true
localhosty false
6 Likes

Was thinking about this more in reference to also blocking external website / non-approved embeds and I think a more general purpose check would be to look at window.top.location.host

Tested and appears to work to block embeds of domains you do not whitelist. Still need to merge this kind of thing with example @britzl posted above if you want to support several domains (and subdomains like www or the lack of).

function init(self)
	local embed_domain = html5.run("window.top.location.host")
	local approved_embed_domain = "defold.com"
	
	if embed_domain ~= approved_embed_domain then
		print("Failed embed domain check!")
		local top_check = html5.run("self === top")
		if not top_check then
			html5.run("top.location = self.location")
		end
	end
end

Instead of redirecting you could also display a nice message, or redirect to a nice message. But the above would redirect to whatever page the game is actually on.

If you want to be sneaky put the check deeper into the game so people who try to pirate your game iframe your game only for their users to redirect to you.

1 Like

Along with changing the location, I would strongly recommend locking out people from your game when an incorrect domain is detected. I do not know if there are functional methods which still allow people to play HTML5 games, but there are apparently ways to block redirection from embedded pages. And use other methods of blocking embed too like mentioned in How to detect hot-linking

Above all test! The worst situation is uploading a copy of your game that is not setup correctly and can be pirated. Don’t let others steal bandwidth and ad revenue from you.

1 Like

@benjames171 I noticed someone could not play your game on the itch.io client due to the domain lock. A workaround is to add support for detecting the protocol.

local html5_protocol = html5.run("location.protocol")
if html5_protocol == "itch-cave:" then -- game is being ran via the itch.io client, let it go
...
2 Likes

Thanks for the tip. Unfortunately, there seems to be a more fundamental issue in that my Defold web games don’t load on the itch.io app (at least on my Windows machine). The loading screen shows and the progress bar is at 100% but then it’s frozen. Older games display the message “Unable to start game, WebGL not supported.”

Strange… I tried a few of mine and they work.

So, the game not working on the itch app problem is to do with my laptop having dedicated graphics as well as onboard. Disabling the dedicated card fixes the issue and the game now loads correctly. It’s a very old problem I’ve encountered before.

The itch app reports the host “game.itch” rather than “itch-cave” so I’ve added that to the safe list and the game is now playable on the app.

1 Like

itch-cave: is the protocol not the host, like http: or https: but it is good to know what host they do use in the app.

function init(self)
	label.set_text("#label", html5.run("location.protocol"))
end

1 Like

As the Web Monetization gamejam is HTML5 focused, I’ve been working on a new version of the DomainLock system.

This still needs testing to be sure it’s all working properly so if you have interest in keeping your games from being stolen by other sites (they WILL steal and slap their ads all over your games if you do not protect them) then please try this and see if you can break it. I’ve done some testing but not enough to feel comfortable saying it’s ready to use.

Through working on this I realized that the old method of Domain Lock was imperfect since there are sneaky things other sites can do, this new version tries to solve those issues.

One thing that you must absolutely do is to put your game in a bricked state if it detects it’s not allowed on a domain. This can be a simple screen saying the game is not allowed on the site, go to your site directly to play it.

A fun thing about the external method of hosting a manifest is that you could initially set it up to allow any site to host your game, and then later on disable that. So then pirate sites would host a version that is bricked and end up sending users directly to you. :smiling_imp:

9 Likes