mirror of
https://git.phreedom.club/localhost_frssoft/fediauth.git
synced 2024-11-24 17:01:29 +00:00
fork creation done
This commit is contained in:
parent
376d163d08
commit
3466db227f
12
.github/workflows/luacheck.yml
vendored
12
.github/workflows/luacheck.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
|
||||
on: [push, pull_request]
|
||||
name: luacheck
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: lint
|
||||
uses: Roang-zero1/factorio-mod-luacheck@master
|
||||
with:
|
||||
luacheckrc_url: ""
|
20
.github/workflows/test.yml
vendored
20
.github/workflows/test.yml
vendored
|
@ -1,20 +0,0 @@
|
|||
name: test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
ENGINE_VERSION: [5.3.0, 5.4.0, 5.5.0, 5.6.1, latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
- name: test
|
||||
run: docker-compose up --exit-code-from test test
|
|
@ -1,5 +1,5 @@
|
|||
globals = {
|
||||
"otp",
|
||||
"fediauth",
|
||||
"minetest"
|
||||
}
|
||||
|
||||
|
@ -13,10 +13,5 @@ read_globals = {
|
|||
"dump", "dump2",
|
||||
"VoxelArea",
|
||||
|
||||
-- testing
|
||||
"mtt"
|
||||
}
|
||||
|
||||
files["qrencode.lua"] = {
|
||||
ignore = {"631"}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
version: "3.6"
|
||||
|
||||
services:
|
||||
test:
|
||||
build: ./test
|
||||
user: root
|
||||
volumes:
|
||||
- "./:/root/.minetest/worlds/world/worldmods/otp/"
|
||||
- "./test/minetest.conf:/minetest.conf"
|
||||
- "world:/root/.minetest/worlds/world"
|
||||
|
||||
minetest:
|
||||
image: registry.gitlab.com/minetest/minetest/server:5.6.1
|
||||
user: root
|
||||
ports:
|
||||
- "30000:30000/udp"
|
||||
volumes:
|
||||
- "./:/root/.minetest/worlds/world/worldmods/otp/"
|
||||
- "world:/root/.minetest/worlds/world"
|
||||
|
||||
volumes:
|
||||
world: {}
|
|
@ -51,7 +51,7 @@ local function lshift(x, by)
|
|||
end
|
||||
|
||||
-- big-endian uint64 of a number
|
||||
function otp.write_uint64_be(v)
|
||||
function fediauth.write_uint64_be(v)
|
||||
local b1 = bitand( rshift(v, 56), 0xFF )
|
||||
local b2 = bitand( rshift(v, 48), 0xFF )
|
||||
local b3 = bitand( rshift(v, 40), 0xFF )
|
||||
|
@ -73,7 +73,7 @@ for _=1,64 do
|
|||
end
|
||||
|
||||
-- hmac generation
|
||||
function otp.hmac(key, message)
|
||||
function fediauth.hmac(key, message)
|
||||
local i_key_pad = ""
|
||||
for i=1,64 do
|
||||
i_key_pad = i_key_pad .. string.char(bitxor(string.byte(key, i) or 0x00, string.byte(i_pad, i)))
|
||||
|
@ -117,16 +117,16 @@ local function left_pad(str, s, len)
|
|||
return str
|
||||
end
|
||||
|
||||
function otp.generate_totp(secret_b32, unix_time)
|
||||
local key = otp.basexx.from_base32(secret_b32)
|
||||
function fediauth.generate_tfediauth(secret_b32, unix_time)
|
||||
local key = fediauth.basexx.from_base32(secret_b32)
|
||||
unix_time = unix_time or os.time()
|
||||
|
||||
local tx = 30
|
||||
local ct = math.floor(unix_time / tx)
|
||||
local counter = otp.write_uint64_be(ct)
|
||||
local counter = fediauth.write_uint64_be(ct)
|
||||
local valid_seconds = ((ct * tx) + tx) - unix_time
|
||||
|
||||
local hmac = otp.hmac(key, counter)
|
||||
local hmac = fediauth.hmac(key, counter)
|
||||
|
||||
-- https://www.rfc-editor.org/rfc/rfc4226#section-5.4
|
||||
local offset = bitand(string.byte(hmac, #hmac), 0xF)
|
||||
|
@ -141,7 +141,7 @@ function otp.generate_totp(secret_b32, unix_time)
|
|||
return padded_code, valid_seconds
|
||||
end
|
||||
|
||||
function otp.create_qr_png(data)
|
||||
function fediauth.create_qr_png(data)
|
||||
local height = #data + 2
|
||||
local width = height
|
||||
|
||||
|
@ -174,7 +174,7 @@ function otp.create_qr_png(data)
|
|||
return minetest.encode_png(width, height, png_data, 2)
|
||||
end
|
||||
|
||||
function otp.generate_secret()
|
||||
function fediauth.generate_secret()
|
||||
local buf = minetest.sha1("" .. math.random(10000), true)
|
||||
local s = ""
|
||||
for i=1,20 do
|
||||
|
@ -184,30 +184,46 @@ function otp.generate_secret()
|
|||
end
|
||||
|
||||
-- get or generate per-player secret b32 ecoded
|
||||
function otp.get_player_secret_b32(name)
|
||||
local secret_b32 = otp.storage:get_string(name .. "_secret")
|
||||
function fediauth.get_player_secret_b32(name)
|
||||
local secret_b32 = fediauth.storage:get_string(name .. "_secret")
|
||||
if secret_b32 == "" then
|
||||
secret_b32 = otp.basexx.to_base32(otp.generate_secret())
|
||||
otp.storage:set_string(name .. "_secret", secret_b32)
|
||||
secret_b32 = fediauth.basexx.to_base32(fediauth.generate_secret())
|
||||
fediauth.storage:set_string(name .. "_secret", secret_b32)
|
||||
end
|
||||
return secret_b32
|
||||
end
|
||||
|
||||
-- returns true if the player is otp enabled _and_ set up properly
|
||||
function otp.is_player_enabled(name)
|
||||
local has_secret = otp.storage:get_string(name .. "_secret") ~= ""
|
||||
local has_priv = minetest.check_player_privs(name, "otp_enabled")
|
||||
-- returns true if the player is fediauth enabled _and_ set up properly
|
||||
function fediauth.is_player_enabled(name)
|
||||
local has_secret = fediauth.storage:get_string(name .. "_secret") ~= ""
|
||||
local has_priv = minetest.check_player_privs(name, "fediauth_enabled")
|
||||
|
||||
return has_secret and has_priv
|
||||
end
|
||||
|
||||
function otp.check_code(secret_b32, code, time)
|
||||
function fediauth.is_player_bypassed(name)
|
||||
local has_priv = minetest.check_player_privs(name, "fediauth_bypass")
|
||||
|
||||
return has_priv
|
||||
end
|
||||
|
||||
function fediauth.check_code(secret_b32, code, time)
|
||||
time = time or os.time()
|
||||
for _, t_offset in ipairs({0, -30, 30}) do
|
||||
local expected_code = otp.generate_totp(secret_b32, time + t_offset)
|
||||
for _, t_offset in ipairs({0, -30, 30, -60, 60}) do
|
||||
local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset)
|
||||
if expected_code == code then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function fediauth.give_code(secret_b32, time)
|
||||
time = time or os.time()
|
||||
local codeseq = {}
|
||||
for _, t_offset in ipairs({60, 30, 0, -30, -60}) do
|
||||
local expected_code = fediauth.generate_tfediauth(secret_b32, time + t_offset)
|
||||
table.insert(codeseq, expected_code)
|
||||
end
|
||||
return codeseq
|
||||
end
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
|
||||
|
||||
mtt.register("otp.hmac", function(callback)
|
||||
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
|
||||
local secret = otp.basexx.from_base32(secret_b32)
|
||||
local unix_time = 1640995200
|
||||
|
||||
local expected_hmac = otp.basexx.from_base64("m04YheEb7i+ThPMUJEfVXVybZFo=")
|
||||
assert(#expected_hmac == 20)
|
||||
|
||||
local tx = 30
|
||||
local ct = math.floor(unix_time / tx)
|
||||
local counter = otp.write_uint64_be(ct)
|
||||
|
||||
assert( #secret == 20 )
|
||||
assert( otp.basexx.to_base64(secret) == "b5JlMIqmiePBi1tYlQYxNWGsqr8=" )
|
||||
assert( #counter == 8 )
|
||||
assert( otp.basexx.to_base64(counter) == "AAAAAANCp0A=" )
|
||||
|
||||
local hmac = otp.hmac(secret, counter)
|
||||
assert(#hmac == 20)
|
||||
|
||||
for i=1,20 do
|
||||
assert( string.byte(expected_hmac,i) == string.byte(hmac, i) )
|
||||
end
|
||||
|
||||
callback()
|
||||
end)
|
||||
|
||||
mtt.register("otp.generate_totp", function(callback)
|
||||
local expected_code = "699847"
|
||||
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
|
||||
local unix_time = 1640995200
|
||||
|
||||
local code, valid_seconds = otp.generate_totp(secret_b32, unix_time)
|
||||
assert(code == expected_code)
|
||||
assert(valid_seconds > 0)
|
||||
|
||||
code, valid_seconds = otp.generate_totp(secret_b32)
|
||||
print("Current code: " .. code .. " valid for " .. valid_seconds .. " seconds")
|
||||
callback()
|
||||
end)
|
||||
|
||||
mtt.register("otp.check_code", function(callback)
|
||||
local expected_code = "699847"
|
||||
local secret_b32 = "N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
|
||||
local unix_time = 1640995200
|
||||
|
||||
assert(otp.check_code(secret_b32, expected_code, unix_time))
|
||||
assert(otp.check_code(secret_b32, expected_code, unix_time+30))
|
||||
assert(otp.check_code(secret_b32, expected_code, unix_time-30))
|
||||
assert(not otp.check_code(secret_b32, expected_code, unix_time-60))
|
||||
assert(not otp.check_code(secret_b32, expected_code, unix_time+60))
|
||||
assert(not otp.check_code(secret_b32, expected_code))
|
||||
|
||||
callback()
|
||||
end)
|
||||
|
||||
mtt.register("otp.create_qr_png", function(callback)
|
||||
local url = "otpauth://totp/abc:myaccount?algorithm=SHA1&digits=6&issuer=abc&period=30&"
|
||||
.. "secret=N6JGKMEKU2E6HQMLLNMJKBRRGVQ2ZKV7"
|
||||
|
||||
local ok, code = otp.qrcode(url)
|
||||
assert(ok)
|
||||
assert(code)
|
||||
|
||||
local png = otp.create_qr_png(code)
|
||||
assert(png)
|
||||
|
||||
local f = io.open(minetest.get_worldpath() .. "/qr.png", "w")
|
||||
f:write(png)
|
||||
f:close()
|
||||
callback()
|
||||
end)
|
||||
|
||||
mtt.register("otp.generate_secret", function(callback)
|
||||
local s = otp.generate_secret()
|
||||
assert(#s == 20)
|
||||
callback()
|
||||
end)
|
29
init.lua
29
init.lua
|
@ -1,25 +1,28 @@
|
|||
local MP = minetest.get_modpath("otp")
|
||||
local MP = minetest.get_modpath("fediauth")
|
||||
local http = minetest.request_http_api()
|
||||
|
||||
-- sanity checks
|
||||
assert(type(minetest.encode_png) == "function")
|
||||
|
||||
otp = {
|
||||
fediauth = {
|
||||
-- mod storage
|
||||
storage = minetest.get_mod_storage(),
|
||||
|
||||
-- baseXX functions
|
||||
basexx = loadfile(MP.."/basexx.lua")(),
|
||||
|
||||
-- qr code
|
||||
qrcode = loadfile(MP.."/qrencode.lua")().qrcode
|
||||
}
|
||||
|
||||
dofile(MP.."/functions.lua")
|
||||
dofile(MP.."/onboard.lua")
|
||||
dofile(MP.."/join.lua")
|
||||
dofile(MP.."/privs.lua")
|
||||
dofile(MP.."/priv_revoke.lua")
|
||||
|
||||
if minetest.get_modpath("mtt") and mtt.enabled then
|
||||
dofile(MP.."/functions.spec.lua")
|
||||
dofile(MP.."/mastoapi.lua")
|
||||
local instance = minetest.settings:get("fediauth.instance")
|
||||
local key = minetest.settings:get("fediauth.api_token")
|
||||
if not instance or not key then
|
||||
minetest.log("warning", "[fediauth] For working fediauth you should specify fediauth.instance and fediauth.api_token")
|
||||
else
|
||||
mastoapi_init(http, instance, key)
|
||||
dofile(MP.."/functions.lua")
|
||||
dofile(MP.."/onboard.lua")
|
||||
dofile(MP.."/join.lua")
|
||||
dofile(MP.."/privs.lua")
|
||||
dofile(MP.."/priv_revoke.lua")
|
||||
end
|
||||
|
||||
|
|
137
join.lua
137
join.lua
|
@ -1,26 +1,73 @@
|
|||
local FORMNAME = "otp-check"
|
||||
local FORMNAME = "fediauth-check"
|
||||
local FORMNAMEFEDI = "fediauth-check-fedi"
|
||||
|
||||
-- time for otp code verification
|
||||
local otp_time = 300
|
||||
-- time for fediauth code verification
|
||||
local fediauth_time = 300
|
||||
|
||||
-- playername => start_time
|
||||
local otp_sessions = {}
|
||||
local fediauth_sessions = {}
|
||||
|
||||
-- Code formspec on join for otp enabled players
|
||||
local formspecfediadd = "size[9,10]" ..
|
||||
"label[1,7;Input your fediverse account handle]" ..
|
||||
"image[1.5,0.6;7,7;fediverse.png]" ..
|
||||
"field[1,9;4,1;fediverse_account_url;@nick@example.com;]" ..
|
||||
"button[5,8.7;3,1;submit;Send code]"
|
||||
|
||||
local feditempstore = {}
|
||||
|
||||
minetest.register_entity("fediauth:checkmark", {
|
||||
initial_properties = {
|
||||
pointable = false,
|
||||
armor_groups = { immortal = 1 },
|
||||
visual = "sprite",
|
||||
visual_size = {x = 0.5, y = 0.5},
|
||||
textures = { "checkmark.png^[opacity:180" },
|
||||
use_texture_alpha = true,
|
||||
static_save = false,
|
||||
glow = 5,
|
||||
},
|
||||
on_detach = function(self, parent) self.object:remove() end
|
||||
})
|
||||
|
||||
function fediauth.verified_checkmark(player, verified)
|
||||
local tag = player:get_player_name()
|
||||
local props = player:get_properties()
|
||||
if verified then
|
||||
local obj = minetest.add_entity({x=0,y=2,z=0}, "fediauth:checkmark", nil)
|
||||
obj:set_attach(player, "Head", {x = 0, y = 12, z = 0})
|
||||
player:set_properties({nametag = props.nametag .. " [FEDI]", nametag_color = "#00ff00" })
|
||||
end
|
||||
end
|
||||
|
||||
-- Code formspec on join for fediauth enabled players
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local playername = player:get_player_name()
|
||||
if otp.is_player_enabled(playername) then
|
||||
minetest.log("action", "[otp] session start for player: '" .. playername .. "'")
|
||||
if fediauth.is_player_bypassed(playername) then return end
|
||||
if fediauth.is_player_enabled(playername) or minetest.settings:get_bool("fediauth.fedi_required", false) then
|
||||
minetest.log("action", "[fediauth] session start for player: '" .. playername .. "'")
|
||||
|
||||
-- start otp session time
|
||||
otp_sessions[player:get_player_name()] = os.time()
|
||||
-- start fediauth session time
|
||||
fediauth_sessions[player:get_player_name()] = os.time()
|
||||
|
||||
-- revoke important privs and re-grant again on code-verification
|
||||
otp.revoke_privs(playername)
|
||||
fediauth.revoke_privs(playername)
|
||||
|
||||
-- if fedi only allowed
|
||||
if minetest.settings:get_bool("fediauth.fedi_required", false) then
|
||||
local existsfedi = fediauth.storage:get_string(playername .. "_fedi")
|
||||
if existsfedi == "" or not existsfedi then
|
||||
minetest.log("action", "[fediauth] request fedi account for player: '" .. playername .. "'")
|
||||
minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local secret_b32 = fediauth.get_player_secret_b32(playername)
|
||||
local codeseq = fediauth.give_code(secret_b32)
|
||||
fediauth.send_code(codeseq[1], fediauth.storage:get_string(playername .. "_fedi"))
|
||||
-- send verification formspec
|
||||
local formspec = "size[10,2]" ..
|
||||
"label[1,0;Please enter your OTP code below]" ..
|
||||
"label[1,0;Please check your fedi account and enter code]" ..
|
||||
"field[1,1.3;4,1;code;Code;]" ..
|
||||
"button_exit[5,1;3,1;submit;Verify]"
|
||||
|
||||
|
@ -28,39 +75,73 @@ minetest.register_on_joinplayer(function(player)
|
|||
end
|
||||
end)
|
||||
|
||||
-- clear otp session on leave
|
||||
-- clear fediauth session on leave
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local playername = player:get_player_name()
|
||||
otp_sessions[playername] = nil
|
||||
fediauth_sessions[playername] = nil
|
||||
end)
|
||||
|
||||
-- check sessions periodically and kick if timed out
|
||||
local function session_check()
|
||||
local now = os.time()
|
||||
for name, start_time in pairs(otp_sessions) do
|
||||
if (now - start_time) > otp_time then
|
||||
minetest.kick_player(name, "OTP Code validation timed out")
|
||||
otp_sessions[name] = nil
|
||||
for name, start_time in pairs(fediauth_sessions) do
|
||||
if (now - start_time) > fediauth_time then
|
||||
minetest.kick_player(name, "fediauth code validation timed out")
|
||||
fediauth_sessions[name] = nil
|
||||
end
|
||||
end
|
||||
minetest.after(5, session_check)
|
||||
end
|
||||
minetest.after(5, session_check)
|
||||
|
||||
-- otp check
|
||||
-- fediauth check
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= FORMNAME then
|
||||
if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
|
||||
return
|
||||
end
|
||||
|
||||
local playername = player:get_player_name()
|
||||
local secret_b32 = otp.get_player_secret_b32(playername)
|
||||
if otp.check_code(secret_b32, fields.code) then
|
||||
minetest.chat_send_player(playername, "OTP Code validation succeeded")
|
||||
otp_sessions[playername] = nil
|
||||
otp.regrant_privs(playername)
|
||||
else
|
||||
minetest.kick_player(playername, "OTP Code validation failed")
|
||||
otp.regrant_privs(playername)
|
||||
local secret_b32 = fediauth.get_player_secret_b32(playername)
|
||||
|
||||
-- check for new player or doesn't have fedi account
|
||||
if fields.fediverse_account_url then
|
||||
if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then
|
||||
minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect"))
|
||||
minetest.show_formspec(playername, FORMNAMEFEDI, formspecfediadd)
|
||||
return
|
||||
end
|
||||
local secret_b32 = fediauth.get_player_secret_b32(playername)
|
||||
local codeseq = fediauth.give_code(secret_b32)
|
||||
fediauth.send_code(codeseq[1], fields.fediverse_account_url)
|
||||
feditempstore[playername] = fields.fediverse_account_url
|
||||
local formspec = "size[9,10]" ..
|
||||
"label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" ..
|
||||
"field[1,9;4,1;code;Code;]" ..
|
||||
"button_exit[5,8.7;3,1;submit;Verify]"
|
||||
|
||||
minetest.show_formspec(playername, FORMNAME, formspec)
|
||||
return
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
|
||||
|
||||
if fediauth.check_code(secret_b32, fields.code) then
|
||||
local fedi_account = fediauth.storage:get_string(playername .. "_fedi")
|
||||
|
||||
-- for account without fediverse (for prevent write account if code incorrect
|
||||
if fedi_account == "" and feditempstore[playername] then
|
||||
fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername])
|
||||
fedi_account = feditempstore[playername]
|
||||
feditempstore[playername] = nil
|
||||
end
|
||||
|
||||
minetest.chat_send_player(playername, minetest.colorize("#00ff00", "fediauth code validation succeeded for " .. fedi_account))
|
||||
fediauth_sessions[playername] = nil
|
||||
fediauth.regrant_privs(playername)
|
||||
fediauth.verified_checkmark(player, true)
|
||||
else
|
||||
minetest.kick_player(playername, "fediauth code validation failed")
|
||||
fediauth.regrant_privs(playername)
|
||||
end
|
||||
end)
|
||||
|
|
135
license.txt
135
license.txt
|
@ -1,25 +1,122 @@
|
|||
License of source code
|
||||
----------------------
|
||||
Creative Commons Legal Code
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (C) 2023 BuckarooBanzay
|
||||
CC0 1.0 Universal
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
software and associated documentation files (the "Software"), to deal in the Software
|
||||
without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
Statement of Purpose
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
For more details:
|
||||
https://opensource.org/licenses/MIT
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
|
|
27
mastoapi.lua
Normal file
27
mastoapi.lua
Normal file
|
@ -0,0 +1,27 @@
|
|||
local http, instance, key
|
||||
|
||||
function fediauth.send_code(code, account_handle)
|
||||
local status = {
|
||||
visibility = "direct",
|
||||
status = "" .. code .. " " .. account_handle .. " code for minetest fediauth, do not share it!"
|
||||
}
|
||||
local json = minetest.write_json(status)
|
||||
http.fetch({
|
||||
url = "https://" .. instance .. "/api/v1/statuses",
|
||||
extra_headers = { "Content-Type: application/json", "Authorization: Bearer " .. key },
|
||||
timeout = 15,
|
||||
post_data = json
|
||||
}, function(res)
|
||||
if res then
|
||||
minetest.log("action", "[fediauth] code sent to: '" .. account_handle .. "'")
|
||||
else
|
||||
minetest.log("error", "[fediauth] code not sent to: '" .. account_handle .. "'")
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function mastoapi_init(h, i, k)
|
||||
http = h
|
||||
instance = i
|
||||
key = k
|
||||
end
|
5
mod.conf
5
mod.conf
|
@ -1,3 +1,2 @@
|
|||
name = otp
|
||||
optional_depends = mtt
|
||||
min_minetest_version = 5.3
|
||||
name = fediauth
|
||||
min_minetest_version = 5.3
|
||||
|
|
105
onboard.lua
105
onboard.lua
|
@ -1,78 +1,81 @@
|
|||
local FORMNAME = "otp-onboard"
|
||||
local FORMNAME = "fediauth-onboard"
|
||||
local FORMNAMEFEDI = "fediauth-onboard-fedi"
|
||||
|
||||
minetest.register_chatcommand("otp_disable", {
|
||||
description = "Disable the otp verification",
|
||||
privs = { otp_enabled = true, interact = true },
|
||||
local feditempstore = {}
|
||||
|
||||
function string.starts(String,Start)
|
||||
return string.sub(String,1,string.len(Start))==Start
|
||||
end
|
||||
|
||||
minetest.register_chatcommand("fediauth_off", {
|
||||
description = "Disable the fediauth verification",
|
||||
privs = { fediauth_enabled = true, interact = true },
|
||||
func = function(name)
|
||||
-- clear priv
|
||||
local privs = minetest.get_player_privs(name)
|
||||
privs.otp_enabled = nil
|
||||
privs.fediauth_enabled = nil
|
||||
minetest.set_player_privs(name, privs)
|
||||
return true, "OTP login disabled"
|
||||
return true, "fediauth login disabled"
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_chatcommand("otp_enable", {
|
||||
description = "Enable the otp verification",
|
||||
minetest.register_chatcommand("fediauth_on", {
|
||||
description = "Enable the fediauth verification",
|
||||
func = function(name)
|
||||
-- issuer name
|
||||
local issuer = minetest.settings:get("otp.issuer") or "Minetest"
|
||||
local server_name = minetest.settings:get("server_name")
|
||||
local server_address = minetest.settings:get("server_address")
|
||||
if server_name and server_name ~= "" then
|
||||
issuer = server_name
|
||||
elseif server_address and server_address ~= "" then
|
||||
issuer = server_address
|
||||
local secret_b32 = fediauth.get_player_secret_b32(name)
|
||||
|
||||
local formspec_account = "size[9,10]" ..
|
||||
"label[1,7;Input your fediverse account handle]" ..
|
||||
"image[1.5,0.6;7,7;fediverse.png]" ..
|
||||
"field[1,9;4,1;fediverse_account_url;@nick@example.com;]" ..
|
||||
"button[5,8.7;3,1;submit;Send code]"
|
||||
|
||||
minetest.show_formspec(name, FORMNAMEFEDI, formspec_account)
|
||||
end
|
||||
|
||||
-- authenticator image
|
||||
local image = minetest.settings:get("otp.authenticator_image") or
|
||||
"https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png"
|
||||
|
||||
local secret_b32 = otp.get_player_secret_b32(name)
|
||||
|
||||
-- url for the qr code
|
||||
local url = "otpauth://totp/" .. issuer .. ":" .. name .. "?algorithm=SHA1" ..
|
||||
"&digits=6" ..
|
||||
"&issuer=" .. issuer ..
|
||||
"&period=30" ..
|
||||
"&secret=" .. secret_b32 ..
|
||||
"&image=" .. image
|
||||
|
||||
local ok, code = otp.qrcode(url)
|
||||
if not ok then
|
||||
return false, "qr code generation failed"
|
||||
end
|
||||
|
||||
local png = otp.create_qr_png(code)
|
||||
local formspec = "size[9,10]" ..
|
||||
"image[1.5,0.6;7,7;^[png:" .. minetest.encode_base64(png) .. "]" ..
|
||||
"label[1,7;Use the above QR code in your OTP-App to obtain a verification code]" ..
|
||||
"field[1,9;4,1;code;Code;]" ..
|
||||
"button_exit[5,8.7;3,1;submit;Verify]"
|
||||
|
||||
minetest.show_formspec(name, FORMNAME, formspec)
|
||||
end
|
||||
})
|
||||
|
||||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||||
if formname ~= FORMNAME then
|
||||
if formname ~= FORMNAME and formname ~= FORMNAMEFEDI then
|
||||
return
|
||||
end
|
||||
|
||||
if fields.fediverse_account_url then
|
||||
local playername = player:get_player_name()
|
||||
if not string.starts(fields.fediverse_account_url, "@") or string.len(fields.fediverse_account_url) < 3 or string.len(fields.fediverse_account_url) > 100 then
|
||||
minetest.chat_send_player(playername, minetest.colorize("#ff0000", "Try again, your input is incorrect"))
|
||||
return
|
||||
end
|
||||
local secret_b32 = fediauth.get_player_secret_b32(playername)
|
||||
local codeseq = fediauth.give_code(secret_b32)
|
||||
fediauth.send_code(codeseq[1], fields.fediverse_account_url)
|
||||
feditempstore[playername] = fields.fediverse_account_url
|
||||
local formspec = "size[9,10]" ..
|
||||
"label[1,7;Check code on " .. minetest.formspec_escape(fields.fediverse_account_url) .. "]" ..
|
||||
"field[1,9;4,1;code;Code;]" ..
|
||||
"button_exit[5,8.7;3,1;submit;Verify]"
|
||||
|
||||
minetest.show_formspec(playername, FORMNAME, formspec)
|
||||
|
||||
end
|
||||
|
||||
if fields.code then
|
||||
local playername = player:get_player_name()
|
||||
local secret_b32 = otp.get_player_secret_b32(playername)
|
||||
if otp.check_code(secret_b32, fields.code) then
|
||||
local secret_b32 = fediauth.get_player_secret_b32(playername)
|
||||
if fediauth.check_code(secret_b32, fields.code) then
|
||||
-- set priv
|
||||
local privs = minetest.get_player_privs(playername)
|
||||
privs.otp_enabled = true
|
||||
privs.fediauth_enabled = true
|
||||
minetest.set_player_privs(playername, privs)
|
||||
fediauth.verified_checkmark(player, true)
|
||||
if feditempstore[playername] then
|
||||
fediauth.storage:set_string(playername .. "_fedi", feditempstore[playername])
|
||||
feditempstore[playername] = nil
|
||||
end
|
||||
|
||||
minetest.chat_send_player(playername, "Code validation succeeded, OTP login enabled")
|
||||
minetest.chat_send_player(playername, "Code validation succeeded, fediauth login enabled")
|
||||
else
|
||||
minetest.chat_send_player(playername, "Code validation failed!")
|
||||
end
|
||||
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
-- privs to revoke until the verification code is validated
|
||||
local temp_revoke_privs = {}
|
||||
|
||||
-- mark player health-related privs as "otp_keep" (they don't get removed while entering the otp code)
|
||||
-- mark player health-related privs as "fediauth_keep" (they don't get removed while entering the fediauth code)
|
||||
for _, name in ipairs({"fly", "noclip"}) do
|
||||
local priv_def = minetest.registered_privileges[name]
|
||||
if priv_def then
|
||||
priv_def.otp_keep = true
|
||||
priv_def.fediauth_keep = true
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_on_mods_loaded(function()
|
||||
-- collect all privs to revoke while entering the otp code
|
||||
-- collect all privs to revoke while entering the fediauth code
|
||||
for name, priv_def in pairs(minetest.registered_privileges) do
|
||||
if not priv_def.otp_keep then
|
||||
if not priv_def.fediauth_keep then
|
||||
-- not marked explicitly as "keep"
|
||||
table.insert(temp_revoke_privs, name)
|
||||
end
|
||||
|
@ -21,9 +21,9 @@ minetest.register_on_mods_loaded(function()
|
|||
end)
|
||||
|
||||
-- moves all "temp_revoke_privs" to mod-storage
|
||||
function otp.revoke_privs(playername)
|
||||
function fediauth.revoke_privs(playername)
|
||||
local privs = minetest.get_player_privs(playername)
|
||||
if otp.storage:get_string(playername .. "_privs") == "" then
|
||||
if fediauth.storage:get_string(playername .. "_privs") == "" then
|
||||
local moved_privs = {}
|
||||
|
||||
for _, priv_name in ipairs(temp_revoke_privs) do
|
||||
|
@ -33,15 +33,15 @@ function otp.revoke_privs(playername)
|
|||
end
|
||||
end
|
||||
|
||||
minetest.log("action", "[otp] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs))
|
||||
minetest.log("action", "[fediauth] revoking privs of '" .. playername .. "' list: " .. dump(moved_privs))
|
||||
minetest.set_player_privs(playername, privs)
|
||||
otp.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs))
|
||||
fediauth.storage:set_string(playername .. "_privs", minetest.serialize(moved_privs))
|
||||
end
|
||||
end
|
||||
|
||||
-- moves all privs from mod-storage into the live privs
|
||||
function otp.regrant_privs(playername)
|
||||
local stored_priv_str = otp.storage:get_string(playername .. "_privs")
|
||||
function fediauth.regrant_privs(playername)
|
||||
local stored_priv_str = fediauth.storage:get_string(playername .. "_privs")
|
||||
if stored_priv_str ~= "" then
|
||||
local privs = minetest.get_player_privs(playername)
|
||||
local stored_privs = minetest.deserialize(stored_priv_str)
|
||||
|
@ -51,8 +51,8 @@ function otp.regrant_privs(playername)
|
|||
privs[priv_name] = true
|
||||
end
|
||||
|
||||
minetest.log("action", "[otp] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs))
|
||||
minetest.log("action", "[fediauth] regranting privs of '" .. playername .. "' list: " .. dump(stored_privs))
|
||||
minetest.set_player_privs(playername, privs)
|
||||
otp.storage:set_string(playername .. "_privs", "")
|
||||
fediauth.storage:set_string(playername .. "_privs", "")
|
||||
end
|
||||
end
|
12
privs.lua
12
privs.lua
|
@ -1,6 +1,12 @@
|
|||
|
||||
minetest.register_privilege("otp_enabled", {
|
||||
description = "otp enabled player",
|
||||
minetest.register_privilege("fediauth_enabled", {
|
||||
description = "fediauth enabled player",
|
||||
give_to_singleplayer = false,
|
||||
otp_keep = true
|
||||
fediauth_keep = true
|
||||
})
|
||||
|
||||
minetest.register_privilege("fediauth_bypass", {
|
||||
description = "fediauth bypass for players who not want type code on each log in (when enabled fediauth.fedi_required)",
|
||||
give_to_singleplayer = false,
|
||||
fediauth_keep = true
|
||||
})
|
||||
|
|
1325
qrencode.lua
1325
qrencode.lua
File diff suppressed because it is too large
Load diff
49
readme.md
49
readme.md
|
@ -1,49 +1,50 @@
|
|||
|
||||
# (T)OTP mod for minetest
|
||||
# FediAuth mod for minetest
|
||||
|
||||
* State: **Stable**
|
||||
2FA via Fediverse account, based on https://content.minetest.net/packages/mt-mods/otp/
|
||||
|
||||
# Overview
|
||||
|
||||
Lets security-aware players use the `/otp_enable` command to protect their account with a second factor.
|
||||
Lets Fediverse players use the `/fediauth_on` command to protect their account with a second factor.
|
||||
|
||||
Players that have the OTP enabled have to enter a verification code upon joining the game.
|
||||
Players that have the FediAuth enabled have to enter a verification code upon joining the game, the code will be sent to their account handle (@nick@example.com).
|
||||
|
||||
# OTP Authenticator apps
|
||||
That mod requires add to `secure.http_mods = fediauth` for sending codes from service account (any mastodon API compatible instance)
|
||||
|
||||
* https://freeotp.github.io/
|
||||
* https://github.com/helloworld1/FreeOTPPlus
|
||||
Add `fediauth.instance = example.com` and `fediauth.api_token` = secret` for work this mod.
|
||||
|
||||
Also you can enable fediauth.fedi_required option and players who not have fediverse account can't play on server
|
||||
|
||||
# Screenshots
|
||||
|
||||
OTP verification form
|
||||
![](./screenshot1.png)
|
||||
FediAuth verification form
|
||||
![](./screenshot1.jpg)
|
||||
|
||||
FediAuth Setup form
|
||||
![](./screenshot2.jpg)
|
||||
|
||||
FediAuth checkmark if verified success
|
||||
![](./screenshot3.jpg)
|
||||
|
||||
OTP Setup form
|
||||
![](./screenshot2.png)
|
||||
|
||||
# Temporary privilege revocation
|
||||
|
||||
All of the privileges get revoked when logging in with the otp enabled (until the proper code is entered).
|
||||
All of the privileges get revoked when logging in with the fediauth enabled (until the proper code is entered).
|
||||
Some exceptions:
|
||||
* `fly` (otherwise the player would literally fall from the sky)
|
||||
* `noclip`
|
||||
|
||||
To disable revokation on custom privs the field `otp_keep` can be set to true on the definition:
|
||||
To disable revokation on custom privs the field `fediauth_keep` can be set to true on the definition:
|
||||
```lua
|
||||
minetest.register_privilege("my_super_important_priv", {
|
||||
description = "something something",
|
||||
otp_keep = true
|
||||
fediauth_keep = true
|
||||
})
|
||||
```
|
||||
|
||||
# Settings
|
||||
|
||||
* `otp.authenticator_image` The image to use in the QR code for the otp app (defaults to "https://raw.githubusercontent.com/minetest/minetest/master/misc/minetest-xorg-icon-128.png")
|
||||
* `otp.issuer` The issuer name, defaults to server name, address or just "Minetest"
|
||||
|
||||
# Links / References
|
||||
|
||||
* https://fedi.tips/
|
||||
* https://en.wikipedia.org/wiki/Time-based_one-time_password
|
||||
* https://en.wikipedia.org/wiki/HMAC-based_one-time_password
|
||||
* https://en.wikipedia.org/wiki/HMAC
|
||||
|
@ -51,16 +52,16 @@ minetest.register_privilege("my_super_important_priv", {
|
|||
|
||||
# Chatcommands
|
||||
|
||||
* `/otp_enable` Starts the OTP onboarding process
|
||||
* `/otp_disable` Disables the OTP Login
|
||||
* `/fediauth_on` Starts the FediAuth
|
||||
* `/fediauth_off` Disables the FediAuth login
|
||||
|
||||
# Privileges
|
||||
|
||||
* `otp_enabled` Players with this privilege have to verify the OTP Code upon login (automatically granted on successful `/otp_enable`)
|
||||
* `fediauth_enabled` Players with this privilege have to verify the Fediverse code upon login (automatically granted on successful `/fediauth_enable`)
|
||||
* `fediauth_bypass` Players with this privilege can bypass verification for any reason, and the privilege can only granted manually by administrator
|
||||
|
||||
# License
|
||||
|
||||
* Code: `MIT`
|
||||
* Code: `CC0-1.0`
|
||||
* Textures: `CC-BY-SA 3.0`
|
||||
* "basexx.lua" `MIT` https://github.com/aiq/basexx/blob/master/lib/basexx.lua
|
||||
* "qrencode.lua" `BSD` https://github.com/speedata/luaqrcode/blob/master/qrencode.lua
|
||||
|
|
BIN
screenshot1.jpg
Normal file
BIN
screenshot1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
BIN
screenshot2.jpg
Normal file
BIN
screenshot2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 172 KiB |
BIN
screenshot3.jpg
Normal file
BIN
screenshot3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
9
settingtypes.txt
Normal file
9
settingtypes.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Instance domain for service account (code sender)
|
||||
# example.com
|
||||
# Must be mastodon API compatible
|
||||
fediauth.instance (Instance domain) string
|
||||
|
||||
fediauth.api_token (Token for account) string
|
||||
|
||||
# If no fediverse account - no access to server
|
||||
fediauth.fedi_required (Require Fediverse account for each user) bool false
|
|
@ -1,11 +0,0 @@
|
|||
ARG ENGINE_VERSION=5.6.1
|
||||
FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION}
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add git &&\
|
||||
mkdir -p /root/.minetest/worlds/world/worldmods/ &&\
|
||||
cd /root/.minetest/worlds/world/worldmods/ &&\
|
||||
git clone https://github.com/BuckarooBanzay/mtt
|
||||
|
||||
ENTRYPOINT minetestserver --config /minetest.conf
|
|
@ -1,3 +0,0 @@
|
|||
default_game = minetest_game
|
||||
mg_name = v7
|
||||
mtt_enable = true
|
BIN
textures/checkmark.png
Normal file
BIN
textures/checkmark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
textures/fediverse.png
Normal file
BIN
textures/fediverse.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Loading…
Reference in a new issue