diff options
author | Lassi Pulkkinen <lassi@pulk.fi> | 2024-10-31 03:11:21 +0200 |
---|---|---|
committer | Lassi Pulkkinen <lassi@pulk.fi> | 2024-10-31 03:51:35 +0200 |
commit | ae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch) | |
tree | 5f462459ae4b47d22114eed717d1382d08cf4dfe /mcproto |
Diffstat (limited to 'mcproto')
-rw-r--r-- | mcproto/+test.ha | 47 | ||||
-rw-r--r-- | mcproto/decode.ha | 193 | ||||
-rw-r--r-- | mcproto/encode.ha | 51 | ||||
-rw-r--r-- | mcproto/error.ha | 1 | ||||
-rw-r--r-- | mcproto/frame.ha | 22 | ||||
-rw-r--r-- | mcproto/handshake.ha | 46 | ||||
-rw-r--r-- | mcproto/packet.ha | 13 | ||||
-rw-r--r-- | mcproto/status/ids.ha | 5 | ||||
-rw-r--r-- | mcproto/status/ping.ha | 9 | ||||
-rw-r--r-- | mcproto/status/response.ha | 111 |
10 files changed, 498 insertions, 0 deletions
diff --git a/mcproto/+test.ha b/mcproto/+test.ha new file mode 100644 index 0000000..5ce070c --- /dev/null +++ b/mcproto/+test.ha @@ -0,0 +1,47 @@ +use bytes; +use trace; + +fn varint_roundtrip(value: i32, bytes: []u8) void = { + let buf: []u8 = []; + encode_varint(&buf, value); + assert(bytes::equal(buf, bytes)); + let dec = Decoder { + input = buf, + pos = 0, + tracer = &trace::silent, + }; + assert(decode_varint(&root(&dec))! == value); +}; + +fn varint_invalid(bytes: []u8) void = { + let dec = Decoder { + input = bytes, + pos = 0, + tracer = &trace::silent, + }; + assert(decode_varint(&root(&dec)) is trace::failed); +}; + +@test fn varints() void = { + // sample varints from https://wiki.vg/protocol?oldid=17184#varint_and_varlong + varint_roundtrip(0, [0x00]); + varint_roundtrip(1, [0x01]); + varint_roundtrip(2, [0x02]); + varint_roundtrip(127, [0x7f]); + varint_roundtrip(128, [0x80, 0x01]); + varint_roundtrip(255, [0xff, 0x01]); + varint_roundtrip(25565, [0xdd, 0xc7, 0x01]); + varint_roundtrip(2097151, [0xff, 0xff, 0x7f]); + varint_roundtrip(2147483647, [0xff, 0xff, 0xff, 0xff, 0x07]); + varint_roundtrip(-1, [0xff, 0xff, 0xff, 0xff, 0x0f]); + varint_roundtrip(-2147483648, [0x80, 0x80, 0x80, 0x80, 0x08]); +}; + +@test fn varints_invalid() void = { + varint_invalid([]); + varint_invalid([0x8f]); + varint_invalid([0x8f, 0x8f]); + varint_invalid([0x80, 0x80, 0x80, 0x80, 0x80, 0x00]); + varint_invalid([0x80, 0x80, 0x80, 0x80, 0x10]); + varint_invalid([0xff, 0xff, 0xff, 0xff, 0xff]); +}; diff --git a/mcproto/decode.ha b/mcproto/decode.ha new file mode 100644 index 0000000..bd020c4 --- /dev/null +++ b/mcproto/decode.ha @@ -0,0 +1,193 @@ +use encoding::utf8; +use endian; +use fmt; +use io; +use strings; +use trace; +use uuid; + +export type Decoder = struct { + input: []u8, + pos: size, + tracer: *trace::tracer, +}; + +export type Context = struct { + dec: *Decoder, + pos: size, + fmt: str, + fields: []fmt::field, + up: nullable *Context, +}; + +export fn log( + ctx: *Context, + lvl: trace::level, + fmt: str, + fields: fmt::field... +) void = { + log_(ctx, null, lvl, fmt, fields...); +}; + +fn log_( + ctx: *Context, + trace_ctx: nullable *trace::context, + lvl: trace::level, + fmt: str, + fields: fmt::field... +) void = { + let s = ""; + defer free(s); + if (len(ctx.fmt) != 0) { + s = fmt::asprintf(ctx.fmt, ctx.fields...); + // TODO: is this legal? works at the moment due to qbe + // semantics, but who knows... + trace_ctx = &trace::context { + fmt = "{} (offset {})", + fields = [s, ctx.pos], + next = trace_ctx, + }; + }; + match (ctx.up) { + case let ctx_: *Context => + log_(ctx_, trace_ctx, lvl, fmt, fields...); + case null => + trace::log(ctx.dec.tracer, trace_ctx, lvl, fmt, fields...); + }; +}; + +export fn error(ctx: *Context, fmt: str, fields: fmt::field...) trace::failed = { + log(ctx, trace::level::ERROR, fmt, fields...); + return trace::failed; +}; + +export fn root(dec: *Decoder) Context = { + return Context { + dec = dec, + pos = dec.pos, + ... + }; +}; + +export fn context(ctx: *Context, fmt: str, fields: fmt::field...) Context = { + return Context { + dec = ctx.dec, + pos = ctx.dec.pos, + fmt = fmt, + fields = fields, + up = ctx, + }; +}; + +export fn decode_nbytes(ctx: *Context, length: size) + ([]u8 | trace::failed) = { + const dec = ctx.dec; + if (len(dec.input) - dec.pos < length) { + return error(ctx, "Expected {} bytes, found only {}", + length, len(dec.input) - dec.pos); + }; + const res = dec.input[dec.pos..dec.pos + length]; + dec.pos += length; + return res; +}; + +export fn decode_byte(ctx: *Context) (u8 | trace::failed) = { + const b = decode_nbytes(ctx, 1)?; + return b[0]; +}; +export fn decode_short(ctx: *Context) (u16 | trace::failed) = { + const b = decode_nbytes(ctx, 2)?; + return endian::begetu16(b); +}; +export fn decode_int(ctx: *Context) (u32 | trace::failed) = { + const b = decode_nbytes(ctx, 4)?; + return endian::begetu32(b); +}; +export fn decode_long(ctx: *Context) (u64 | trace::failed) = { + const b = decode_nbytes(ctx, 8)?; + return endian::begetu64(b); +}; + +export fn decode_bool(ctx: *Context) (bool | trace::failed) = { + const b = decode_byte(ctx)?; + if (b >= 2) { + return error(ctx, "Invalid boolean"); + }; + return b != 0; +}; + +export fn decode_float(ctx: *Context) (f32 | trace::failed) = { + const v = decode_int(ctx)?; + return *(&v: *f32); +}; +export fn decode_double(ctx: *Context) (f64 | trace::failed) = { + const v = decode_long(ctx)?; + return *(&v: *f64); +}; + +export fn try_decode_varint(ctx: *Context) (i32 | !(trace::failed | TooShort)) = { + let res = 0u32; + const dec = ctx.dec; + + for (let i = 0u32; dec.pos + i < len(dec.input); i += 1) { + const b = dec.input[dec.pos + i]; + + if (i == 4 && b & 0xf0 != 0) { + return error(ctx, "VarInt too long"); + }; + + res |= (b & 0x7f): u32 << (7 * i); + + if (b & 0x80 == 0) { + dec.pos += i + 1; + return res: i32; + }; + }; + + return TooShort; +}; + +export fn decode_varint(ctx: *Context) (i32 | trace::failed) = { + match (try_decode_varint(ctx)) { + case let res: i32 => + return res; + case => + return error(ctx, "VarInt too short"); + }; +}; + +export fn decode_string(ctx: *Context, maxlen: size) (str | trace::failed) = { + const length = decode_varint(&context(ctx, "string length"))?; + const length = length: size; + + if (length >= maxlen * 4) { + return error(ctx, + "String length {} exceeds limit of {} bytes", + length, maxlen * 4); + }; + + const ctx_ = context(ctx, "string data ({} bytes)", length); + const bytes = decode_nbytes(&ctx_, length)?; + match (strings::fromutf8(bytes)) { + case let string: str => + // don't bother checking length in code points. doesn't seem + // very useful. + return string; + case utf8::invalid => + return error(&ctx_, "Invalid UTF-8"); + }; +}; + +export fn decode_uuid(ctx: *Context) (uuid::uuid | trace::failed) = { + let uuid: [16]u8 = [0...]; + uuid[..] = decode_nbytes(ctx, 16)?; + return uuid; +}; + +export fn expect_end(ctx: *Context) (void | trace::failed) = { + if (ctx.dec.pos != len(ctx.dec.input)) { + return error(ctx, + "Expected end of input, but found {} extra bytes starting at {}", + len(ctx.dec.input) - ctx.dec.pos, ctx.dec.pos); + }; +}; diff --git a/mcproto/encode.ha b/mcproto/encode.ha new file mode 100644 index 0000000..de30871 --- /dev/null +++ b/mcproto/encode.ha @@ -0,0 +1,51 @@ +use endian; +use strings; +use uuid; + +export fn encode_short(out: *[]u8, v: u16) void = { + const buf: [2]u8 = [0...]; + endian::beputu16(buf, v); + append(out, buf...); +}; +export fn encode_int(out: *[]u8, v: u32) void = { + const buf: [4]u8 = [0...]; + endian::beputu32(buf, v); + append(out, buf...); +}; +export fn encode_long(out: *[]u8, v: u64) void = { + const buf: [8]u8 = [0...]; + endian::beputu64(buf, v); + append(out, buf...); +}; + +export fn encode_bool(out: *[]u8, v: bool) void = { + append(out, if (v) 1 else 0); +}; + +export fn encode_float(out: *[]u8, v: f32) void = { + encode_int(out, *(&v: *u32)); +}; +export fn encode_double(out: *[]u8, v: f64) void = { + encode_long(out, *(&v: *u64)); +}; + +export fn encode_varint(out: *[]u8, v: i32) void = { + let v = v: u32; + + for (true) { + const continues = v & 0x7f != v; + const high_bit: u8 = if (continues) 0x80 else 0x00; + append(out, (v & 0x7f): u8 | high_bit); + v >>= 7; + if (!continues) return; + }; +}; + +export fn encode_string(out: *[]u8, string: str) void = { + encode_varint(out, len(string): i32); + append(out, strings::toutf8(string)...); +}; + +export fn encode_uuid(out: *[]u8, uuid: uuid::uuid) void = { + append(out, uuid...); +}; diff --git a/mcproto/error.ha b/mcproto/error.ha new file mode 100644 index 0000000..092210d --- /dev/null +++ b/mcproto/error.ha @@ -0,0 +1 @@ +export type TooShort = !void; diff --git a/mcproto/frame.ha b/mcproto/frame.ha new file mode 100644 index 0000000..d5ee4c1 --- /dev/null +++ b/mcproto/frame.ha @@ -0,0 +1,22 @@ +use bufio; +use io; +use trace; + +export fn write_frame(out: io::handle, in: []u8) (void | io::error) = { + assert(len(in) <= 0x1fffff, "write_frame: too long"); + let length_buf: [3]u8 = [0...]; + let length_buf = length_buf[..0]; + encode_varint(&length_buf, len(in): i32); + io::writeall(out, length_buf)?; + io::writeall(out, in)?; +}; + +export fn write_packet(out: io::handle, packet_id: i32, payload: []u8) + (void | io::error) = { + static let packet_buf: [0x1fffff]u8 = [0...]; + + let packet = packet_buf[..0]; + encode_packet(&packet, packet_id, payload); + + write_frame(out, packet)?; +}; diff --git a/mcproto/handshake.ha b/mcproto/handshake.ha new file mode 100644 index 0000000..8df5254 --- /dev/null +++ b/mcproto/handshake.ha @@ -0,0 +1,46 @@ +use trace; + +export def SB_HANDSHAKE: i32 = 0x00; + +export type Handshake = struct { + proto_ver: i32, + server_addr: str, + server_port: u16, + next_state: NextState, +}; + +export type NextState = enum i32 { + STATUS = 1, + LOGIN = 2, +}; + +export fn encode_handshake(out: *[]u8, pkt: *Handshake) void = { + encode_varint(out, pkt.proto_ver); + encode_string(out, pkt.server_addr); + encode_short(out, pkt.server_port); + encode_varint(out, pkt.next_state); +}; + +export fn decode_handshake(ctx: *Context) (Handshake | trace::failed) = { + const ctx_ = context(ctx, "protocol version"); + const proto_ver = decode_varint(&ctx_)?; + const ctx_ = context(ctx, "server address"); + const server_addr = decode_string(&ctx_, 255)?; + const ctx_ = context(ctx, "server port"); + const server_port = decode_short(&ctx_)?; + const ctx_ = context(ctx, "next state"); + const next_state = decode_varint(&ctx_)?: NextState; + if (next_state != NextState::STATUS + && next_state != NextState::LOGIN) { + return error(&ctx_, "Invalid next state 0x{:02x}", + next_state: i32); + }; + expect_end(ctx)?; + + return Handshake { + proto_ver = proto_ver, + server_addr = server_addr, + server_port = server_port, + next_state = next_state, + }; +}; diff --git a/mcproto/packet.ha b/mcproto/packet.ha new file mode 100644 index 0000000..669df56 --- /dev/null +++ b/mcproto/packet.ha @@ -0,0 +1,13 @@ +use math; +use trace; + +export fn encode_packet(out: *[]u8, packet_id: i32, in: []u8) void = { + let id_size = math::bit_size_u32(packet_id: u32); + if (id_size == 0) id_size = 1; + const id_size = id_size / 7 + id_size % 7; + assert(len(in) + id_size <= 0x1fffff, + "TODO: sent packet too long. this should be handled better."); + + encode_varint(out, packet_id); + append(out, in...); +}; diff --git a/mcproto/status/ids.ha b/mcproto/status/ids.ha new file mode 100644 index 0000000..89d6bb4 --- /dev/null +++ b/mcproto/status/ids.ha @@ -0,0 +1,5 @@ +export def SB_REQUEST: i32 = 0x00; +export def SB_PING: i32 = 0x01; + +export def CB_RESPONSE: i32 = 0x00; +export def CB_PONG: i32 = 0x01; diff --git a/mcproto/status/ping.ha b/mcproto/status/ping.ha new file mode 100644 index 0000000..00ff7cf --- /dev/null +++ b/mcproto/status/ping.ha @@ -0,0 +1,9 @@ +use mcproto; +use trace; + +export fn decode_ping(ctx: *mcproto::Context) (i64 | trace::failed) = { + const ctx_ = mcproto::context(ctx, "ping id"); + const ping_id = mcproto::decode_long(&ctx_)?: i64; + mcproto::expect_end(ctx)?; + return ping_id; +}; diff --git a/mcproto/status/response.ha b/mcproto/status/response.ha new file mode 100644 index 0000000..e93df47 --- /dev/null +++ b/mcproto/status/response.ha @@ -0,0 +1,111 @@ +use dejson; +use encoding::json; +use mcproto; +use strings; +use trace; +use uuid; + +export type Response = struct { + description: str, + version_name: str, + version_protocol: i32, + players_online: u32, + players_max: u32, + players_sample: [](str, uuid::uuid), + previews_chat: bool, + forces_reportable_chat: bool, +}; + +export fn decode_response(ctx: *mcproto::Context) (Response | trace::failed) = { + const ctx_ = mcproto::context(ctx, "json"); + const json = mcproto::decode_string(&ctx_, 32767)?; + mcproto::expect_end(ctx)?; + + const json = match (json::loadstr(json)) { + case let json: json::value => + yield json; + case let err: json::error => + return mcproto::error(&ctx_, "Invalid JSON: {}", json::strerror(err)); + }; + defer json::finish(json); + + const deser = dejson::newdeser(&json); + match (deser_response(&deser)) { + case let res: Response => + return res; + case let err: dejson::error => + defer free(err); + return mcproto::error(&ctx_, "Invalid response contents: {}", err); + }; +}; + +export fn deser_response(de: *dejson::deser) (Response | dejson::error) = { + let success = false; + + let res = Response { ... }; + defer if (!success) response_finish(res); + + res.description = match (dejson::optfield(de, "description")?) { + case let de_description: dejson::deser => + yield json::dumpstr(*de_description.val); + case void => + yield ""; + }; + + const de_version = dejson::field(de, "version")?; + res.version_name = strings::dup(dejson::string( + &dejson::field(&de_version, "name")?)?); + res.version_protocol = dejson::number_i32( + &dejson::field(&de_version, "protocol")?)?; + + const de_players = dejson::field(de, "players")?; + res.players_online = dejson::number_u32( + &dejson::field(&de_players, "online")?)?; + res.players_max = dejson::number_u32( + &dejson::field(&de_players, "max")?)?; + match (dejson::optfield(&de_players, "sample")?) { + case let de_sample: dejson::deser => + const nsample = dejson::length(&de_sample)?; + for (let i = 0z; i < nsample; i += 1) { + const de_player = dejson::index(&de_sample, i)?; + const name = dejson::string( + &dejson::field(&de_player, "name")?)?; + const de_id = dejson::field(&de_player, "id")?; + const id = dejson::string(&de_id)?; + const id = match (uuid::decodestr(id)) { + case let id: uuid::uuid => + yield id; + case uuid::invalid => + return dejson::fail(&de_id, "Invalid UUID"); + }; + append(res.players_sample, (strings::dup(name), id)); + }; + case void => void; + }; + + res.previews_chat = match (dejson::optfield(de, "previewsChat")) { + case let de_previews_chat: dejson::deser => + yield dejson::boolean(&de_previews_chat)?; + case void => + yield false; + }; + res.forces_reportable_chat = match ( + dejson::optfield(de, "enforcesSecureChat")) { + case let de_forces_reportable_chat: dejson::deser => + yield dejson::boolean(&de_forces_reportable_chat)?; + case void => + yield false; + }; + + success = true; + return res; +}; + +export fn response_finish(res: Response) void = { + free(res.description); + free(res.version_name); + for (let i = 0z; i < len(res.players_sample); i += 1) { + free(res.players_sample[i].0); + }; + free(res.players_sample); +}; |