some minor refactoring

This commit is contained in:
Jill 2022-12-30 19:04:27 +03:00
commit 4ca2ba06ab
29 changed files with 953 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

1
.env.example Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=sqlite3://./crystalgauntlet.db

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/lib/
/bin/
/.shards/
*.dwarf
.env
crystalgauntlet.db

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"crystal-lang.completion": true,
"crystal-lang.hover": true,
"crystal-lang.implementations": true,
"crystal-lang.mainFile": "${workspaceRoot}/src/crystal-gauntlet.cr"
}

16
Cakefile Normal file
View File

@ -0,0 +1,16 @@
# todo: move inside executable
require "log"
require "dotenv"
require "sqlite3"
require "migrate"
Dotenv.load
desc "Migrate database to the latest version"
task :dbmigrate do
migrator = Migrate::Migrator.new(
DB.open(ENV["DATABASE_URL"])
)
migrator.to_latest
end

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Jill "oatmealine" Monoids <oatmealine@disroot.org>
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# crystal-gauntlet
among balls
## build
`shards build`
you may need to head into `lib/` to fix deps. i'm Very sorry
## setup
copy `.env.example` to `.env` and fill it out
run `cake db:migrate` (must have [cake](https://github.com/axvm/cake/))
**schemas are highly unstable so you will be offered 0 support in migrating databases for now**, however in the future you'll want to run this each time you update
then `bin/crystal-gauntlet` (or `shards run`)
### real
![real](docs/crystal-gauntlet.jpg)

View File

@ -0,0 +1,47 @@
-- +migrate up
CREATE TABLE levels (
id SERIAL PRIMARY KEY,
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
modified_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
name TEXT NOT NULL,
user_id INTEGER NOT NULL references users(id),
description TEXT NOT NULL DEFAULT "",
original INTEGER,
game_version INTEGER NOT NULL,
binary_version INTEGER NOT NULL,
password TEXT,
requested_stars INTEGER,
unlisted INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 0,
level_data BLOB NOT NULL,
extra_data BLOB NOT NULL,
level_info BLOB NOT NULL,
-- checksums, presumably
wt1 TEXT NOT NULL,
wt2 TEXT NOT NULL,
song_id INTEGER NOT NULL,
length INTEGER NOT NULL,
objects INTEGER NOT NULL,
coins INTEGER NOT NULL DEFAULT 0,
has_ldm INTEGER NOT NULL DEFAULT 0,
two_player INTEGER NOT NULL DEFAULT 0,
downloads INTEGER NOT NULL DEFAULT 0,
likes INTEGER NOT NULL DEFAULT 0,
difficulty INTEGER,
demon_difficulty INTEGER,
stars INTEGER,
featured INTEGER NOT NULL DEFAULT 0,
epic INTEGER NOT NULL DEFAULT 0,
rated_coins INTEGER NOT NULL DEFAULT 0
);
-- +migrate down
DROP TABLE levels;

49
db/migrations/2_users.sql Normal file
View File

@ -0,0 +1,49 @@
-- +migrate up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
-- on a registered account, account_id refers to the
-- account ID - however, pre 2.0, instead udid referred
-- to the user's UUID or UDID, depending on platform.
-- UUID and UDID are unique ids assigned for green
-- username users
--
-- in short, if `registered`, use account_id, else, use udid
udid TEXT,
account_id INTEGER references accounts(id),
registered INTEGER NOT NULL,
username TEXT NOT NULL,
stars INTEGER NOT NULL DEFAULT 0,
demons INTEGER NOT NULL DEFAULT 0,
coins INTEGER NOT NULL DEFAULT 0,
user_coins INTEGER NOT NULL DEFAULT 0,
diamonds INTEGER NOT NULL DEFAULT 0,
orbs INTEGER NOT NULL DEFAULT 0,
creator_points INTEGER NOT NULL DEFAULT 0,
completed_levels INTEGER NOT NULL DEFAULT 0,
icon_type INTEGER NOT NULL DEFAULT 0, -- icon to display in comments, etc
color1 INTEGER NOT NULL DEFAULT 0,
color2 INTEGER NOT NULL DEFAULT 3,
cube INTEGER NOT NULL DEFAULT 0,
ship INTEGER NOT NULL DEFAULT 0,
ball INTEGER NOT NULL DEFAULT 0,
ufo INTEGER NOT NULL DEFAULT 0,
wave INTEGER NOT NULL DEFAULT 0,
robot INTEGER NOT NULL DEFAULT 0,
spider INTEGER NOT NULL DEFAULT 0,
explosion INTEGER NOT NULL DEFAULT 0,
special INTEGER NOT NULL DEFAULT 0,
glow INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
last_played TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now')),
is_banned INTEGER NOT NULL DEFAULT 0,
is_banned_upload INTEGER NOT NULL DEFAULT 0
);
-- +migrate down
DROP TABLE users;

View File

@ -0,0 +1,24 @@
-- +migrate up
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL, -- bcrypt hashed
gjp2 TEXT NOT NULL,
email TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
messages_enabled INTEGER NOT NULL DEFAULT 1, -- messages from non-friends enabled
friend_requests_enabled INTEGER NOT NULL DEFAULT 1, -- frs enabled
comments_enabled INTEGER NOT NULL DEFAULT 0, -- able to see user's comments
youtube_url TEXT,
twitter_url TEXT,
twitch_url TEXT,
created_at TEXT NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'now'))
);
-- +migrate down
DROP TABLE accounts;

BIN
docs/crystal-gauntlet.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

5
docs/exe-penis.txt Normal file
View File

@ -0,0 +1,5 @@
http://www.boomlings.com/database ->
http://localhost:8080/asdfasdfasd
aHR0cDovL3d3dy5ib29tbGluZ3MuY29tL2RhdGFiYXNl -> base64 http://localhost:8080/asdfasdfasd
aHR0cDovL2xvY2FsaG9zdDo4MDgwL2FzZGZhc2RmYXNk

22
shard.lock Normal file
View File

@ -0,0 +1,22 @@
version: 2.0
shards:
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.6.0
dotenv:
git: https://github.com/gdotdesign/cr-dotenv.git
version: 0.1.0
migrate:
git: https://github.com/vladfaust/migrate.cr.git
version: 0.5.0
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.13.0
time_format:
git: https://github.com/vladfaust/time_format.cr.git
version: 0.1.1

23
shard.yml Normal file
View File

@ -0,0 +1,23 @@
name: crystal-gauntlet
version: 0.1.0
authors:
- Jill "oatmealine" Monoids <oatmealine@disroot.org>
- winter <me@wint0r.zone>
targets:
crystal-gauntlet:
main: src/crystal-gauntlet.cr
dependencies:
sqlite3:
github: crystal-lang/crystal-sqlite3
migrate:
github: vladfaust/migrate.cr
version: ~> 0.5.0
dotenv:
github: gdotdesign/cr-dotenv
crystal: 1.6.2
license: MIT

View File

@ -0,0 +1,9 @@
require "./spec_helper"
describe Crystal::Gauntlet do
# TODO: Write tests
it "works" do
false.should eq(true)
end
end

2
spec/spec_helper.cr Normal file
View File

@ -0,0 +1,2 @@
require "spec"
require "../src/crystal-gauntlet"

31
src/accounts.cr Normal file
View File

@ -0,0 +1,31 @@
require "uri"
include CrystalGauntlet
module CrystalGauntlet::Accounts
extend self
def get_ext_id_from_params(params : URI::Params) : String
return "1"
if params.has_key?("udid") && params["udid"] != ""
# todo: numeric id check
params["udid"]
elsif params.has_key?("account_id") && params["account_id"] != "" && params["account_id"] != "0"
# todo: validate password
params["account_id"]
else
"-1"
end
end
def get_user_id(username : String, ext_id : String) : Int32
return 1
DATABASE.query("select id from users where udid = ? or account_id = ?", ext_id, ext_id) do |rs|
if rs.column_count > 0
return rs.read(Int32)
else
raise "no user associated with account?!"
end
end
end
end

56
src/crystal-gauntlet.cr Normal file
View File

@ -0,0 +1,56 @@
require "http/server"
require "uri"
require "sqlite3"
require "migrate"
require "dotenv"
require "./enums"
require "./hash"
require "./format"
require "./accounts"
require "./gjp"
Dotenv.load
module CrystalGauntlet
VERSION = "0.1.0"
APPEND_PATH = "asdfasdfasd/"
DATABASE = DB.open(ENV["DATABASE_URL"])
@@endpoints = Hash(String, (String -> String)).new
def self.endpoints
@@endpoints
end
def self.run()
server = HTTP::Server.new do |context|
# expunge trailing slashes
path = context.request.path.chomp("/")
path = path.sub(APPEND_PATH, "")
body = context.request.body
if !body
puts "no body :("
elsif @@endpoints.has_key?(path)
func = @@endpoints[path]
value = func.call(body.gets_to_end)
context.response.content_type = "text/plain"
context.response.print value
puts "#{path} -> #{value}"
else
context.response.respond_with_status(404, "endpoint not found")
puts "#{path} -> 404"
end
end
puts "Listening on http://127.0.0.1:8080"
server.listen(8080)
end
end
require "./endpoints/**"
CrystalGauntlet.run()

View File

@ -0,0 +1,27 @@
require "uri"
require "base64"
require "crypto/bcrypt/password"
include CrystalGauntlet
CrystalGauntlet.endpoints["/accounts/loginGJAccount.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
username = params["userName"]
password = params["password"]
result = DATABASE.query_all("select id, password from accounts", 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(username, account_id.to_s)
"#{account_id},#{user_id}"
else
return "-12"
end
else
return "-1"
end
}

View File

@ -0,0 +1,28 @@
require "uri"
require "base64"
require "crypto/bcrypt/password"
include CrystalGauntlet
CrystalGauntlet.endpoints["/accounts/registerGJAccount.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
username = params["userName"]
password = params["password"]
email = params["email"]
username_exists = DATABASE.scalar "select count(*) from accounts where username = ?", username
if username_exists != 0
return "-2"
end
password_hash = Crypto::Bcrypt::Password.create(password, cost: 10).to_s
gjp2 = CrystalGauntlet::GJP.hash(password)
next_id = (DATABASE.scalar("select max(id) from accounts").as(Int64 | Nil) || 0) + 1
DATABASE.exec "insert into accounts (id, username, password, email, gjp2) values (?, ?, ?, ?, ?)", next_id, username, password_hash, email, gjp2
user_id = (DATABASE.scalar("select max(id) from users").as(Int64 | Nil) || 0) + 1
DATABASE.exec "insert into users (id, account_id, username, registered) values (?, ?, ?, 1)", user_id, next_id, username
"1"
}

View File

@ -0,0 +1,111 @@
require "uri"
require "base64"
include CrystalGauntlet
CrystalGauntlet.endpoints["/downloadGJLevel22.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
response = ""
DATABASE.query("select levels.id, levels.name, levels.level_data, levels.extra_data, levels.level_info, levels.password, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered from levels join users on levels.user_id = users.id where levels.id = ?", params["levelID"].to_i32) do |rs|
if rs.move_next
id = rs.read(Int32)
name = rs.read(String)
level_data = rs.read(String)
extra_data = rs.read(String)
level_info = rs.read(String)
password = rs.read(String | Nil)
user_id = rs.read(Int32)
description = rs.read(String)
original = rs.read(Int32 | Nil)
game_version = rs.read(Int32)
requested_stars = rs.read(Int32 | Nil)
version = rs.read(Int32)
song_id = rs.read(Int32)
length = rs.read(Int32)
objects = rs.read(Int32)
coins = rs.read(Int32)
has_ldm = rs.read(Bool)
two_player = rs.read(Bool)
downloads = rs.read(Int32)
likes = rs.read(Int32)
difficulty_int = rs.read(Int32 | Nil)
difficulty = difficulty_int && LevelDifficulty.new(difficulty_int)
demon_difficulty_int = rs.read(Int32 | Nil)
demon_difficulty = demon_difficulty_int && DemonDifficulty.new(demon_difficulty_int)
stars = rs.read(Int32 | Nil)
featured = rs.read(Bool)
epic = rs.read(Bool)
rated_coins = rs.read(Bool)
user_username = rs.read(String)
user_udid = rs.read(String | Nil)
user_account_id = rs.read(Int32 | Nil)
user_registered = rs.read(Bool)
xor_pass = "0"
if !password
password = "0"
elsif params["gameVersion"].to_i >= 20
xor_pass = GDBase64.encode(XorCrypt.encrypt_string(password, "26364"))
else
xor_pass = password
end
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266
response += CrystalGauntlet::Format.fmt_hash({
1 => id,
2 => name,
3 => Base64.encode(description).sub('/', '_').sub('+', '-').strip("\n"),
4 => level_data,
5 => version,
6 => user_id,
8 => 10,
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
10 => downloads,
11 => 1,
12 => song_id < 50 ? song_id : 0,
13 => game_version,
14 => likes,
17 => difficulty && difficulty.demon?,
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
25 => difficulty && difficulty.auto?,
18 => stars || 0,
19 => featured,
42 => epic,
45 => objects,
15 => length,
30 => original || 0,
31 => two_player,
28 => "1",
29 => "1",
35 => song_id >= 50 ? song_id : 0,
36 => extra_data,
37 => coins,
38 => rated_coins,
39 => requested_stars || 0,
46 => 1,
47 => 2,
40 => has_ldm,
27 => xor_pass,
# 0 for n/a, 10 for easy, 20, for medium, ...
})
if params.has_key?("extras")
response += ":26:" + level_info
end
response += "#" + Hashes.gen_solo(level_data)
thing = [user_id, stars || 0, difficulty && difficulty.demon?, id, rated_coins, featured, password, 0].map! { |x| Format.fmt_value(x) }
puts thing.join(",")
response += "#" + Hashes.gen_solo_2(thing.join(","))
else
response += "-1"
end
end
response
}

View File

@ -0,0 +1,93 @@
require "uri"
require "base64"
include CrystalGauntlet
CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
results = [] of String
users = [] of String
songs = [] of String
hash_data = [] of Tuple(Int32, Int32, Bool)
DATABASE.query "select levels.id, levels.name, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered from levels join users on levels.user_id = users.id" do |rs|
rs.each do
id = rs.read(Int32)
name = rs.read(String)
user_id = rs.read(Int32)
description = rs.read(String)
original = rs.read(Int32 | Nil)
game_version = rs.read(Int32)
requested_stars = rs.read(Int32 | Nil)
version = rs.read(Int32)
song_id = rs.read(Int32)
length = rs.read(Int32)
objects = rs.read(Int32)
coins = rs.read(Int32)
has_ldm = rs.read(Bool)
two_player = rs.read(Bool)
downloads = rs.read(Int32)
likes = rs.read(Int32)
difficulty_int = rs.read(Int32 | Nil)
difficulty = difficulty_int && LevelDifficulty.new(difficulty_int)
demon_difficulty_int = rs.read(Int32 | Nil)
demon_difficulty = demon_difficulty_int && DemonDifficulty.new(demon_difficulty_int)
stars = rs.read(Int32 | Nil)
featured = rs.read(Bool)
epic = rs.read(Bool)
rated_coins = rs.read(Bool)
user_username = rs.read(String)
user_udid = rs.read(String | Nil)
user_account_id = rs.read(Int32 | Nil)
user_registered = rs.read(Bool)
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266
results << CrystalGauntlet::Format.fmt_hash({
1 => id,
2 => name,
5 => version,
6 => user_id,
8 => 10,
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
10 => downloads,
12 => song_id < 50 ? song_id : 0,
13 => game_version,
14 => likes,
17 => difficulty && difficulty.demon?,
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
25 => difficulty && difficulty.auto?,
18 => stars || 0,
19 => featured,
42 => epic,
45 => objects,
3 => Base64.encode(description).sub('/', '_').sub('+', '-').strip("\n"),
15 => length,
30 => original || 0,
31 => two_player,
37 => coins,
38 => rated_coins,
39 => requested_stars || 0,
46 => 1,
47 => 2,
40 => has_ldm,
35 => song_id >= 50 ? song_id : 0, # 0 for n/a, 10 for easy, 20, for medium, ...
})
users << "#{user_id}:#{user_username}:#{user_registered ? user_account_id : user_udid}"
hash_data << {id, stars || 0, rated_coins}
end
end
# `:${offset}:${levelsPerPage}`
searchMeta = "#{results.size}:0:10"
res = [results.join("|"), users.join("|"), songs.join("|"), searchMeta, CrystalGauntlet::Hashes.gen_multi(hash_data)].join("#")
puts res
res
}

View File

@ -0,0 +1,35 @@
require "uri"
include CrystalGauntlet
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["8369"], "gjp" => ["Vw9mW0FBUgN_VXtZ"], "userName" => ["oatmealine"], "levelID" => ["0"], "levelName" => ["security"], "levelDesc" => [""], "levelVersion" => ["1"], "levelLength" => ["1"], "audioTrack" => ["0"], "auto" => ["0"], "password" => ["0"], "original" => ["0"], "twoPlayer" => ["0"], "songID" => ["1050575"], "objects" => ["207"], "coins" => ["0"], "requestedStars" => ["0"], "unlisted" => ["0"], "wt" => ["709"], "wt2" => ["0"], "ldm" => ["0"], "extraString" => ["0_73_0_39_0_0_0_0_0_0_0_0_66_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0"], "seed" => ["SbpPMJ9vjn"], "seed2" => ["UFIKAAQCA1RTB1cABFEMUlBRDwFRUgMPAlAHVFYMUQFXAwEGCAAPDQ=="], "levelString" => ["H4sIAAAAAAAAC61Xy5HYIAxtyLuDJCTB5JQatgAKSAspPvxssC1lc8jBZngPhNAP-PVF6YASpWBhKlQkFoCCzAVwNNSbWD6gSIEQQtECBbj9UgklFfgNtcX6Uf2-mQ7udAhYxuhvRGRTRBszJvyTkLrfYusyBAE2Qe3_jSD-X4LEEXT8-gl0hNbwaGQ08ah_OaB1dECzSa35otx72P9DQid-xv4fbJ3dGzjCDzhAYzzwoHDQAbyA3IDYgdoD4qtL1HiQhmj91dU-ucIHNDbCTmIl8MB46JjZydxICHOq9rldlUAXLXlMCBVB7BMApjLViLtumDpdl8QmI8SuRlhMTEe1WRpLILbdxqnCpXGkKaQh0BCBU-xLzS7j4iuEXfM1oyr8IW3bH7TPPjcg-_Ktr6dFth0MkYNRWDuoqAZjfMPJwcWTPyXt8gdOb7zvWofzaNipxcnYdO5AMzoE2UyJHUm6mWpCp602u5xTL8NAyPaOANAj2COSQyB4RPQIfahJjkNqAHuE5ZJOGDvGsK_ysFnEhzLRs0D0LMCWBYa_gZGW-JGg3LefX8EEHF_ELAdh1IOKpFE88i2FZx-2RUScRYQ8IryIUT9A-WGiCWzLaXKkKjpEAo94W2ESL7uR9IIXljFmCRxbOdXN-a5_zSHbkxgc32MwfI8hbNQ9rBCcrEBwsgLBqmK9fIeH-uhkBaKTFYhGVkTdhMsqzw0lz0DkGYic5MDoGSJahuj-w1gLZ6Uwr_MUuSuKtUbK8ZEvTQc8CypRn86yYRuQ-R7cfbA88v8EZAHpyr4ecKh6P0D3PvZz8xlwacvXljwXpNeQjGPI00yZHTy98Sl6iFKTYp9Kb6rfbMBUgEJ0cH3jYWPydSNYi89FLL3mOjaltsoQbNX2a1jvizMue7adok1thnSbEp_K9h7QjgdCOx4I3_EgEpakRNtVM2wKoBstcy2bcqKFnGghJ1rIiJa5BPkxQX5MkBMTlLNTbirVyk3aLmtTVjTc1nE0ZI17sUGcwhxHRydYIzm4E7TRD9roR2Y04nmtsmroFN9i3BBinTu3bd85yuMVgfrZSOFP5A2OaMJsj1Z7dLqPPtVh6wA7OefUI07G3teMty_YSVJ2i_acYvqI_QxlJw3nVgyN2XcjG2f4NCcH08oMpk-Y7NHRHi326DxggNjef_sDjsS5VJA4tysy34hL1NtV4hQs8QuW-AVLjKp0Uu9aNi0gYdgr3Qxwkoh_IelvM8V0gyTTDZIfidRAdWqWOjXLe3GT9-Qm883dCetJO02pfp1T_9xW_3DWd82eZlEwraV2SVO7pKld0tQuafosaUv5daVRzU8P1IOvEmm-2Wo0jNszmijRwHv9qGrsGBtY2rBxmibQ_TT9A3vViWgxFQAA"], "levelInfo" => ["H4sIAAAAAAAACyXQyxHAIAgE0I6YAPJx7L-v7MIl5ImA-snRp--T0w_f8EFcIs8gB7WZGvSiY9CD68Stx35P5WM1QhM2y-JBzESEImaiSvI_d1cZcZkw-dDMN-Mz3Xegy0XNke8CR0wJ60EYUTro816yUhEuthV7KoIGMUcr8SQiBolMb-sWw8UUz8DeiqugH0LOuW2zJoXVH1ldIOdMAQAA"], "secret" => ["Wmfd2893gb7"]}
CrystalGauntlet.endpoints["/uploadGJLevel21.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
ext_id = Accounts.get_ext_id_from_params(params)
if ext_id == "-1"
return "-1"
end
user_id = Accounts.get_user_id(params["userName"], ext_id)
song_id = params["songID"] == "0" ? params["audioTrack"] : params["songID"]
description = params["levelDesc"]
if params["gameVersion"].to_i >= 20 # 2.0
description = GDBase64.decode description
end
if DATABASE.scalar("select count(*) from levels where id = ? and user_id = ?", params["levelID"], params["accountID"]).as(Int64) > 0
# update existing level
raise "not implemented"
else
# create new level
next_id = (DATABASE.scalar("select max(id) from levels").as(Int64 | Nil) || 0) + 1
DATABASE.exec("insert into levels (id, name, user_id, description, original, game_version, binary_version, password, requested_stars, unlisted, version, level_data, extra_data, level_info, wt1, wt2, song_id, length, objects, coins, has_ldm, two_player) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", next_id, params["levelName"], user_id, description, params["original"].to_i32, params["gameVersion"].to_i32, params["binaryVersion"].to_i32, params["password"] == "0" ? nil : params["password"].to_i32, params["requestedStars"].to_i32, params["unlisted"].to_i32, params["levelVersion"].to_i32, params["levelString"], params["extraString"], params["levelInfo"], params["wt"], params["wt2"], song_id.to_i32, params["levelLength"].to_i32, params["objects"].to_i32, params["coins"].to_i32, params["ldm"].to_i32, params["twoPlayer"].to_i32)
next_id.to_s
end
}

View File

@ -0,0 +1,89 @@
require "uri"
include CrystalGauntlet
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["1"], "gjp" => ["XFZBX1NSW1xcUw=="], "targetAccountID" => ["1"], "secret" => ["Wmfd2893gb7"]}
CrystalGauntlet.endpoints["/getGJUserInfo20.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
DATABASE.query("select accounts.id, accounts.username, is_admin, messages_enabled, friend_requests_enabled, comments_enabled, youtube_url, twitter_url, twitch_url, accounts.created_at, users.id, stars, demons, coins, user_coins, diamonds, orbs, creator_points, icon_type, color1, color2, glow, cube, ship, ball, ufo, wave, robot, spider, explosion from accounts join users on accounts.id = users.account_id where accounts.id = ?", params["targetAccountID"]) do |rs|
if rs.move_next
id = rs.read(Int32)
username = rs.read(String)
is_admin = rs.read(Int32)
messages_enabled = rs.read(Int32)
friend_requests_enabled = rs.read(Int32)
comments_enabled = rs.read(Int32)
youtube_url = rs.read(String | Nil)
twitter_url = rs.read(String | Nil)
twitch_url = rs.read(String | Nil)
created_at = rs.read(String)
user_id = rs.read(Int32)
stars = rs.read(Int32)
demons = rs.read(Int32)
coins = rs.read(Int32)
user_coins = rs.read(Int32)
diamonds = rs.read(Int32)
orbs = rs.read(Int32)
creator_points = rs.read(Int32)
icon_type = rs.read(Int32)
color1 = rs.read(Int32)
color2 = rs.read(Int32)
glow = rs.read(Int32)
cube = rs.read(Int32)
ship = rs.read(Int32)
ball = rs.read(Int32)
ufo = rs.read(Int32)
wave = rs.read(Int32)
robot = rs.read(Int32)
spider = rs.read(Int32)
explosion = rs.read(Int32)
return CrystalGauntlet::Format.fmt_hash({
1 => username,
2 => user_id,
13 => coins,
17 => user_coins,
10 => color1,
11 => color2,
3 => stars,
46 => diamonds,
4 => demons,
8 => creator_points,
18 => !messages_enabled,
19 => !friend_requests_enabled,
50 => !comments_enabled,
20 => youtube_url || "",
21 => cube,
22 => ship,
23 => ball,
24 => ufo,
25 => wave,
26 => robot,
28 => glow,
43 => spider,
47 => explosion,
30 => 1, # rank; todo
16 => id,
# 31 = isnt (0) or is (1) friend or (3) incoming request or (4) outgoing request
# todo
31 => 0,
# also w/ friend requests:
# 32 => id,
# 35 => comment,
# 37 => date,
44 => twitter_url || "",
45 => twitch_url || "",
29 => 1,
# badge, todo
49 => 0
})
end
end
"-1"
# echo "1:".$user["userName"].":2:".$user["userID"].":13:".$user["coins"].":17:".$user["userCoins"].":10:".$user["color1"].":11:".$user["color2"].":3:".$user["stars"].":46:".$user["diamonds"].":4:".$user["demons"].":8:".$creatorpoints.":18:".$msgstate.":19:".$reqsstate.":50:".$commentstate.":20:".$accinfo["youtubeurl"].":21:".$user["accIcon"].":22:".$user["accShip"].":23:".$user["accBall"].":24:".$user["accBird"].":25:".$user["accDart"].":26:".$user["accRobot"].":28:".$user["accGlow"].":43:".$user["accSpider"].":47:".$user["accExplosion"].":30:".$rank.":16:".$user["extID"].":31:".$friendstate.":44:".$accinfo["twitter"].":45:".$accinfo["twitch"].":29:1:49:".$badge . $appendix;
}

View File

@ -0,0 +1,17 @@
require "uri"
include CrystalGauntlet
# URI::Params{"gameVersion" => ["21"], "binaryVersion" => ["35"], "gdw" => ["0"], "accountID" => ["1"], "gjp" => ["XFZBX1NSW1xcUw=="], "targetAccountID" => ["1"], "secret" => ["Wmfd2893gb7"]}
CrystalGauntlet.endpoints["/updateGJUserScore22.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
account_id = Accounts.get_ext_id_from_params(params)
user_id = Accounts.get_user_id(params["userName"], account_id)
DATABASE.exec("update users set username=?, stars=?, demons=?, coins=?, user_coins=?, diamonds=?, icon_type=?, color1=?, color2=?, cube=?, ship=?, ball=?, ufo=?, wave=?, robot=?, spider=?, explosion=?, special=?, glow=?, last_played=? where id=?", params["userName"], params["stars"], params["demons"], params["coins"], params["userCoins"], params["diamonds"], params["iconType"], params["color1"], params["color2"], params["accIcon"], params["accShip"], params["accBall"], params["accBird"], params["accDart"], params["accRobot"], params["accSpider"], params["accExplosion"], params["special"], params["accGlow"], Time.utc.to_s("%Y-%m-%d %H:%M:%S"), user_id)
user_id.to_s
}

63
src/enums.cr Normal file
View File

@ -0,0 +1,63 @@
module CrystalGauntlet
enum LevelLength
Tiny
Short
Medium
Long
XL
end
enum LevelDifficulty
Auto
Easy
Normal
Hard
Harder
Insane
Demon
def to_star_difficulty
case self
when .auto?
50
when .easy?
10
when .normal?
20
when .hard?
30
when .harder?
40
when .insane?
50
when .demon?
50
end
end
end
enum DemonDifficulty
Easy
Medium
Hard
Insane
Extreme
# unsafe
#Tarsorado
def to_demon_difficulty
case self
when .easy?
3
when .medium?
4
when .hard?
0
when .insane?
5
when .extreme?
6
end
end
end
end

50
src/format.cr Normal file
View File

@ -0,0 +1,50 @@
module CrystalGauntlet::Format
extend self
def fmt_value(v) : String
case v
when Bool
v ? "1" : "0"
when String
v
else
v.to_s
end
end
def fmt_hash(hash) : String
hash.map_with_index{ |(i, v)| "#{i}:#{fmt_value(v)}" }.join(":")
end
end
module CrystalGauntlet::GDBase64
extend self
def encode(v)
Base64.encode(v).sub('/', '_').sub('+', '-')
end
def decode(v)
Base64.decode_string(v.sub('_', '/').sub('-', '+'))
end
end
module CrystalGauntlet::XorCrypt
extend self
def encrypt(x : Bytes, key : Bytes) : Bytes
result = Bytes.new(x.size)
x.each.with_index() do |chr, index|
result[index] = (chr ^ key[index % key.size])
end
result
end
def encrypt_string(x : String, key : String) : Bytes
result = Bytes.new(x.bytesize)
x.bytes.each.with_index() do |chr, index|
result[index] = (chr ^ key.byte_at(index % key.bytesize))
end
result
end
end

34
src/gjp.cr Normal file
View File

@ -0,0 +1,34 @@
require "crypto/bcrypt/password"
require "base64"
module CrystalGauntlet::GJP
extend self
XOR_KEY = "37526"
def decrypt(pass : String)
pwd = Base64.decode_string(pass.sub('_', '/').sub('-', '+'))
decrypted = ""
pwd.each.with_index() do |chr, index|
decrypted += (chr ^ XOR_KEY.byte_at(index % XOR_KEY.bytesize)).unsafe_chr
end
decrypted
end
def encrypt(pass : String)
encrypted = Bytes.new(pass.bytesize)
pass.bytes.each.with_index() do |chr, index|
encrypted[index] = chr ^ XOR_KEY.byte_at(index % XOR_KEY.bytesize)
end
Base64.encode(encrypted).sub('/', '_').sub('+', '-')
end
def hash(pass : String)
gjp2_hash = Digest::SHA1.hexdigest(pass + "mI29fmAnxgTs")
Crypto::Bcrypt::Password.create(gjp2_hash, cost: 10).to_s
end
end

56
src/hash.cr Normal file
View File

@ -0,0 +1,56 @@
require "digest/sha1"
require "crypto/bcrypt"
module CrystalGauntlet::Hashes
extend self
def gen_multi(level_hash_data : Array(Tuple(Int32, Int32, Bool)))
Digest::SHA1.hexdigest do |ctx|
level_hash_data.each.with_index() do |val, index|
level_id, stars, coins = val
level_id_str = level_id.to_s
ctx.update "#{level_id_str[0]}#{level_id_str[-1]}#{stars}#{coins ? 1 : 0}"
end
ctx.update "xI25fpAapCQg"
end
end
def gen_solo(level_string : String) : String
hash = ""
divided : Int32 = (level_string.size / 40).to_i
i = 0
k : Int32 = 0
while k < level_string.size
if i > 39
break
end
hash += level_string.char_at(k)
i += 1
k += divided
end
Digest::SHA1.hexdigest(hash.ljust(5, 'a') + "xI25fpAapCQg")
end
def gen_solo_2(level_multi_string : String) : String
Digest::SHA1.hexdigest do |ctx|
ctx.update level_multi_string
ctx.update "xI25fpAapCQg"
end
end
def gen_solo_3(level_multi_string : String) : String
Digest::SHA1.hexdigest do |ctx|
ctx.update level_multi_string
ctx.update "oC36fpYaPtdg"
end
end
def gen_solo_4(level_multi_string : String) : String
Digest::SHA1.hexdigest do |ctx|
ctx.update level_multi_string
ctx.update "pC26fpYaQCtg"
end
end
end