crystal-gauntlet/src/endpoints/levels/uploadLevel.cr

159 lines
6.3 KiB
Crystal

require "uri"
include CrystalGauntlet
CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::Context): String {
params = URI::Params.parse(context.request.body.not_nil!.gets_to_end)
LOG.debug { params.inspect }
# todo: green user fixes? pretty please?
user_id, account_id = Accounts.auth(params)
if !(user_id && account_id)
user_id, account_id = Accounts.auth_old(context.request, params)
if !(user_id && account_id)
return "-1"
end
end
song_id = params["songID"] == "0" ? params["audioTrack"] : params["songID"]
description = params["levelDesc"]
if Versions.parse(params["gameVersion"]) >= Versions::V2_0
description = Clean.clean_special(Base64.decode_string description)
else
description = Clean.clean_special(description)
end
# todo: patch descriptions to prevent color bugs
# todo: use 1.9 levelInfo..?
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/uploadGJLevel.php#L56
# todo: use 2.2 unlisted
extraString = params["extraString"]? || Level::DEFAULT_EXTRA_STRING
original = (params["original"]? || "0").to_i32
if original == 0
original = nil
end
if config_get("levels.parsing.enabled").as?(Bool)
# todo: parse ldm
# todo: parse level length
LOG.debug { "parsing objects" }
level_raw_objects = Level.decode(params["levelString"])
level_objects = Level.to_objectdata(level_raw_objects)
objects = level_objects.size
forbidden_objects = config_get("levels.parsing.object_blocklist").as?(Array(TOML::Type))
if forbidden_objects
# stupid hack; i think this is a crystal compiler bug
forbidden_objects = forbidden_objects.map { |v| v.as?(Int64) }.compact
else
forbidden_objects = [] of Int64
end
allowed_objects = config_get("levels.parsing.object_allowlist").as?(Array(TOML::Type))
if allowed_objects
allowed_objects = allowed_objects.map { |v| v.as?(Int64) }.compact
else
allowed_objects = [] of Int64
end
LOG.debug { "forbidden objects: #{forbidden_objects.inspect}" }
LOG.debug { "allowed objects: #{allowed_objects.inspect}" }
if forbidden_obj = level_objects.find do |obj|
if allowed_objects.size > 0
if !allowed_objects.includes?(obj.id)
true
end
end
if forbidden_objects.includes?(obj.id)
true
end
end
LOG.info { "preventing upload of level with forbidden obj #{forbidden_obj.id}" }
return "-1"
end
if exploit_obj = level_objects.find { |obj|
# target color ID
(obj.target_color_id.try { |n| n < 0 } || obj.target_color_id.try { |n| n > 1100 } ) ||
# target group ID
(obj.target_group_id.try { |n| n < 0 } || obj.target_group_id.try { |n| n > 1100 } )
}
LOG.info { "preventing upload of level attempting to exploit invalid color/group IDs" }
return "-1"
end
coins = level_objects.count { |obj| obj.id == 1329 } # user coin id
# todo: check if dual portals even exist?
two_player = false
level_raw_objects.each do |obj|
if !obj.has_key?("1") && obj["kA10"]? == "1"
two_player = true
end
end
else
objects = params["objects"].to_i
coins = params["coins"].to_i
two_player = params["twoPlayer"].to_i == 1
end
level_length = params["levelLength"].to_i.clamp(0..4)
if coins < 0 || coins > 3
LOG.info { "preventing upload of level with #{coins} coins" }
return "-1"
end
if objects <= 0
LOG.info { "preventing upload of 0-object level" }
return "-1"
end
max_objects = config_get("levels.max_objects").as?(Int64)
if max_objects != nil && max_objects.not_nil! > 0 && objects > max_objects.not_nil!
LOG.info { "preventing upload of level with #{objects} objects (max #{max_objects})" }
return "-1"
end
# todo: check seed2?
requested_stars = (params["requestedStars"]? || "0").to_i.clamp(0..10)
if requested_stars == 0
requested_stars = nil
end
if DATABASE.scalar("select count(*) from levels where id = ?", params["levelID"]).as(Int64) > 0
# update existing level
level_user_id = DATABASE.query_one("select user_id from levels where id = ?", params["levelID"].to_i, as: {Int32})
if level_user_id != user_id
return "-1"
end
DATABASE.exec("update levels set description = ?, password = ?, requested_stars = ?, version = ?, extra_data = ?, level_info = ?, editor_time = ?, editor_time_copies = ?, song_id = ?, length = ?, objects = ?, coins = ?, has_ldm = ?, two_player = ?, modified_at = ? where id = ?", description[..140-1], params["password"] == "0" ? nil : params["password"].to_i, requested_stars, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player, Time.utc.to_s(Format::TIME_FORMAT), params["levelID"].to_i)
File.write(DATA_FOLDER / "levels" / "#{params["levelID"]}.lvl", Base64.decode(params["levelString"]))
return params["levelID"]
else
# create new level
next_id = IDs.get_next_id("levels")
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, extra_data, level_info, editor_time, editor_time_copies, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, Clean.clean_basic(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, (params["binaryVersion"]? || "0").to_i, params["password"] == "0" ? nil : params["password"].to_i, requested_stars, (params["unlisted"]? || "0").to_i == 1, params["levelVersion"].to_i, Clean.clean_special(extraString), Clean.clean_b64(params["levelInfo"]? || ""), (params["wt"]? || "0").to_i, (params["wt2"]? || "0").to_i, song_id.to_i, level_length, objects, coins, (params["ldm"]? || "0").to_i == 1, two_player)
File.write(DATA_FOLDER / "levels" / "#{next_id.to_s}.lvl", Base64.decode(params["levelString"]))
return next_id.to_s
end
}
CrystalGauntlet.endpoints["/uploadGJLevel20.php"] = CrystalGauntlet.endpoints["/uploadGJLevel21.php"]
CrystalGauntlet.endpoints["/uploadGJLevel19.php"] = CrystalGauntlet.endpoints["/uploadGJLevel21.php"]