159 lines
6.5 KiB
Crystal
159 lines
6.5 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)
|
|
inner_level_string = level_raw_objects.find! { |obj| !obj.has_key?("1") && obj["kA9"]? == "0" }
|
|
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 = inner_level_string["kA10"]? == "1"
|
|
|
|
# todo: currently brokey
|
|
#level_length_secs = Level.measure_length(level_objects, inner_level_string["kA4"]?.try &.to_i? || 0)
|
|
#LOG.debug { "level is #{level_length_secs}s long" }
|
|
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"]
|
|
|