diff --git a/config.example.toml b/config.example.toml index 4873576..243ddb6 100644 --- a/config.example.toml +++ b/config.example.toml @@ -37,6 +37,17 @@ spam_thres = -3 # allow new accounts to be created allow_registration = true +[sessions] +# allow sessions to be created (for 1.9, as it +# doesn't send the password for authentication +# in any shape or form, making relying on ip +# addresses the only secure way of accessing +# the server) +allow = true + +# how long the session should last for +expiry_time = 604800 + [reuploads] # allow reuploading levels from other servers allowed = true @@ -240,4 +251,4 @@ allow = true [songs.sources.generic] # direct URLs and similar -allow = true \ No newline at end of file +allow = true diff --git a/public/template/create_session.ecr b/public/template/create_session.ecr new file mode 100644 index 0000000..db03ec8 --- /dev/null +++ b/public/template/create_session.ecr @@ -0,0 +1,30 @@ + + + + + + + + Session Creation + + + <%= Templates.dir_header %> +

Session Creation

+ + <%- if disabled -%> + Sessions have been disabled

+ <%- end -%> + +
+ Username: + Password: + /> +
+ + <%- if result == false -%> +
Could not create session

+ <%- elsif result == true -%> +
Session created successfully
+ <%- end -%> + + diff --git a/public/template/index.ecr b/public/template/index.ecr index 6a10520..b62036c 100644 --- a/public/template/index.ecr +++ b/public/template/index.ecr @@ -38,6 +38,9 @@
  • The Git repository
  • Song reuploading and searching
  • Levels and level reuploading
  • + <%- if config_get("sessions.allow").as(Bool | Nil) -%> +
  • The session creation page (for accessing features in 1.9)
  • + <%- end -%>
  • Completely legal executable download
  • diff --git a/src/endpoints/comments/addLevelComment.cr b/src/endpoints/comments/addLevelComment.cr index 3642733..eb346e9 100644 --- a/src/endpoints/comments/addLevelComment.cr +++ b/src/endpoints/comments/addLevelComment.cr @@ -8,7 +8,10 @@ CrystalGauntlet.endpoints["/uploadGJComment21.php"] = ->(context : HTTP::Server: user_id, account_id = Accounts.auth(params) if !(user_id && account_id) - return "-1" + user_id, account_id = Accounts.auth_old(context.request, params) + if !(user_id && account_id) + return "-1" + end end comment = params["comment"]? @@ -20,7 +23,12 @@ CrystalGauntlet.endpoints["/uploadGJComment21.php"] = ->(context : HTTP::Server: end if comment && !comment.blank? - comment_value = Base64.decode_string(comment)[..100-1] + comment_value = comment + if params.has_key?("gameVersion") + comment_value = Base64.decode_string(comment_value)[..100-1] + else + comment_value = comment_value[..100-1] + end next_id = IDs.get_next_id("comments") DATABASE.exec("insert into comments (id, level_id, user_id, comment, percent) values (?, ?, ?, ?, ?)", next_id, level_id, user_id, comment_value, percent) return "1" @@ -31,6 +39,5 @@ CrystalGauntlet.endpoints["/uploadGJComment21.php"] = ->(context : HTTP::Server: CrystalGauntlet.endpoints["/uploadGJComment20.php"] = CrystalGauntlet.endpoints["/uploadGJComment21.php"] -CrystalGauntlet.endpoints["/uploadGJComment19.php"] = ->(context : HTTP::Server::Context): String { - "-1" -} +CrystalGauntlet.endpoints["/uploadGJComment19.php"] = CrystalGauntlet.endpoints["/uploadGJComment21.php"] + diff --git a/src/endpoints/comments/deleteLevelComment.cr b/src/endpoints/comments/deleteLevelComment.cr index 9af9cbf..3295d03 100644 --- a/src/endpoints/comments/deleteLevelComment.cr +++ b/src/endpoints/comments/deleteLevelComment.cr @@ -8,7 +8,10 @@ CrystalGauntlet.endpoints["/deleteGJComment20.php"] = ->(context : HTTP::Server: user_id, account_id = Accounts.auth(params) if !(user_id && account_id) - return "-1" + user_id, account_id = Accounts.auth_old(context.request, params) + if !(user_id && account_id) + return "-1" + end end comment_user_id, level_user_id = DATABASE.query_one("select comments.user_id, levels.user_id from comments join levels on levels.id = comments.id where comments.id = ?", params["commentID"].to_i, as: {Int32, Int32}) @@ -22,6 +25,4 @@ CrystalGauntlet.endpoints["/deleteGJComment20.php"] = ->(context : HTTP::Server: return "1" } -CrystalGauntlet.endpoints["/deleteGJComment19.php"] = ->(context : HTTP::Server::Context): String { - "-1" -} +CrystalGauntlet.endpoints["/deleteGJComment19.php"] = CrystalGauntlet.endpoints["/deleteGJComment20.php"] diff --git a/src/endpoints/levels/deleteLevel.cr b/src/endpoints/levels/deleteLevel.cr index d1abd84..2fe9f91 100644 --- a/src/endpoints/levels/deleteLevel.cr +++ b/src/endpoints/levels/deleteLevel.cr @@ -7,7 +7,10 @@ CrystalGauntlet.endpoints["/deleteGJLevelUser20.php"] = ->(context : HTTP::Serve user_id, account_id = Accounts.auth(params) if !(user_id && account_id) - return "-1" + user_id, account_id = Accounts.auth_old(context.request, params) + if !(user_id && account_id) + return "-1" + end end level_id = params["levelID"].to_i @@ -19,3 +22,5 @@ CrystalGauntlet.endpoints["/deleteGJLevelUser20.php"] = ->(context : HTTP::Serve return "1" } + +CrystalGauntlet.endpoints["/deleteGJLevelUser19.php"] = CrystalGauntlet.endpoints["/deleteGJLevelUser20.php"] diff --git a/src/endpoints/levels/rateLevel.cr b/src/endpoints/levels/rateLevel.cr index eb8c06c..4bc6670 100644 --- a/src/endpoints/levels/rateLevel.cr +++ b/src/endpoints/levels/rateLevel.cr @@ -87,9 +87,7 @@ CrystalGauntlet.endpoints["/rateGJDemon21.php"] = ->(context : HTTP::Server::Con CrystalGauntlet.endpoints["/rateGJStars20.php"] = CrystalGauntlet.endpoints["/rateGJStars211.php"] -CrystalGauntlet.endpoints["/rateGJStars.php"] = ->(context : HTTP::Server::Context): String { - "-1" -} +CrystalGauntlet.endpoints["/rateGJStars.php"] = CrystalGauntlet.endpoints["/rateGJStars211.php"] CrystalGauntlet.endpoints["/rateGJLevel.php"] = ->(context : HTTP::Server::Context): String { "-1" diff --git a/src/endpoints/levels/uploadLevel.cr b/src/endpoints/levels/uploadLevel.cr index f29bb7a..044e074 100644 --- a/src/endpoints/levels/uploadLevel.cr +++ b/src/endpoints/levels/uploadLevel.cr @@ -9,7 +9,10 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C # todo: green user fixes? pretty please? user_id, account_id = Accounts.auth(params) if !(user_id && account_id) - return "-1" + 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"] @@ -112,7 +115,7 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C # todo: check seed2? - requested_stars = params["requestedStars"].to_i.clamp(0..10) + requested_stars = (params["requestedStars"]? || "0").to_i.clamp(0..10) if requested_stars == 0 requested_stars = nil end @@ -134,7 +137,7 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C # 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_special(params["levelName"])[..20-1], user_id, description[..140-1], params["original"].to_i, params["gameVersion"].to_i, params["binaryVersion"].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) + 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_special(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"])) @@ -144,6 +147,5 @@ CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(context : HTTP::Server::C CrystalGauntlet.endpoints["/uploadGJLevel20.php"] = CrystalGauntlet.endpoints["/uploadGJLevel21.php"] -CrystalGauntlet.endpoints["/uploadGJLevel19.php"] = ->(context : HTTP::Server::Context): String { - "-1" -} +CrystalGauntlet.endpoints["/uploadGJLevel19.php"] = CrystalGauntlet.endpoints["/uploadGJLevel21.php"] + diff --git a/src/endpoints/misc/likeItem.cr b/src/endpoints/misc/likeItem.cr index fdbe238..5fc6d6e 100644 --- a/src/endpoints/misc/likeItem.cr +++ b/src/endpoints/misc/likeItem.cr @@ -44,7 +44,5 @@ CrystalGauntlet.endpoints["/likeGJItem211.php"] = ->(context : HTTP::Server::Con } CrystalGauntlet.endpoints["/likeGJItem20.php"] = CrystalGauntlet.endpoints["/likeGJItem211.php"] +CrystalGauntlet.endpoints["/likeGJItem.php"] = CrystalGauntlet.endpoints["/likeGJItem211.php"] -CrystalGauntlet.endpoints["/likeGJItem.php"] = ->(context : HTTP::Server::Context): String { - "-1" -} diff --git a/src/lib/accounts.cr b/src/lib/accounts.cr index 56bdc55..514e4d6 100644 --- a/src/lib/accounts.cr +++ b/src/lib/accounts.cr @@ -26,6 +26,7 @@ module CrystalGauntlet::Accounts # todo: clean this periodically AUTH_CACHE = Hash(Tuple(String | Nil, String | Nil, String | Nil), Tuple(Int32, Int32) | Tuple(Nil, Nil)).new + SESSIONS = Hash(Tuple(String | Nil, String | Nil), Tuple(Int32, Int32, Int64)).new # returns userid, accountid def auth(params : URI::Params) : (Tuple(Int32, Int32) | Tuple(Nil, Nil)) @@ -52,6 +53,51 @@ module CrystalGauntlet::Accounts return user_id, ext_id.to_i end + def auth_old(req : HTTP::Request, params : URI::Params) : (Tuple(Int32, Int32) | Tuple(Nil, Nil)) + account_id = params["accountID"] + ip = IPs.get_real_ip(req) + + if SESSIONS.has_key?({account_id, ip}) + LOG.debug {"#{account_id || "???"}: session exists"} + user_id, ext_id, expiry_time = SESSIONS[{account_id, ip}] + if Time.utc.to_unix > expiry_time + LOG.debug {"#{account_id || "???"}: session expired"} + SESSIONS.delete({account_id, ip}) + return nil, nil + else + LOG.debug {"#{account_id || "???"}: session valid"} + return user_id, ext_id + end + end + + LOG.debug {"#{account_id || "???"}: session does not exist"} + return nil, nil + end + + def new_session(req : HTTP::Request, username : String, password : String) : Bool + if !config_get("sessions.allow").as(Bool | Nil) + return false + end + + ip = IPs.get_real_ip(req) + result = DATABASE.query_all("select id, password from accounts where username = ?", username, as: {Int32, String}) + if result.size > 0 + account_id, hash = result[0] + bcrypt = Crypto::Bcrypt::Password.new(hash) + + if bcrypt.verify(password) + user_id = Accounts.get_user_id(account_id) + expiry_time = Time.utc.to_unix + (config_get("sessions.expiry_time").as(Int64 | Nil) || 604800) + SESSIONS[{account_id.to_s, ip}] = { user_id, account_id, expiry_time } + return true + else + return false + end + else + return false + end + end + def get_user_id(ext_id : Int32) : Int32 DATABASE.query("select id from users where udid = ? or account_id = ?", ext_id, ext_id) do |rs| if rs.move_next diff --git a/src/template_endpoints/create_session.cr b/src/template_endpoints/create_session.cr new file mode 100644 index 0000000..407cd6f --- /dev/null +++ b/src/template_endpoints/create_session.cr @@ -0,0 +1,22 @@ +require "uri" +require "compress/gzip" + +include CrystalGauntlet + +CrystalGauntlet.template_endpoints["/tools/create_session"] = ->(context : HTTP::Server::Context) { + disabled = !config_get("sessions.allow").as(Bool | Nil) + result = nil + body = context.request.body + if body && !disabled + begin + params = URI::Params.parse(body.gets_to_end) + result = Accounts.new_session(context.request, params["username"], params["password"]) + + rescue error + LOG.error {"whar.... #{error}"} + end + end + + context.response.content_type = "text/html" + ECR.embed("./public/template/create_session.ecr", context.response) +}