The client sends it every HTTP request and its IMQ connection; it makes them with current TLS and certificates, upgrades IMVU's plain-HTTP hosts to HTTPS, and refuses IMVU's reporting endpoints. CI tests it and publishes t5de-proxy.exe as nightlies and tagged releases. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
2551888ed6
19 files changed
+1498
No files matched your search
@@ -0,0 +1,140 @@
|
||||
// Package imq reads the framing of IMQ, IMVU's real-time messaging protocol (chat, presence, room state).
|
||||
//
|
||||
// The stream is a sequence of frames, each a protobuf message with the message type in field 1 (varint) and the
|
||||
// message itself in field 2 (bytes), with nothing between frames. Only the framing is decoded here: enough to log
|
||||
// what goes by, by type and size, without the messages' contents.
|
||||
package imq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Message types, as the client numbers them: C2g are sent by the client, G2c by the gateway.
|
||||
var typeNames = map[uint64]string{
|
||||
1301: "C2gPing",
|
||||
1302: "C2gConnect",
|
||||
1303: "C2gChallengeResponse",
|
||||
1304: "C2gSendMessage",
|
||||
1305: "C2gStateChange",
|
||||
1306: "C2gOpenFloodgates",
|
||||
1307: "C2gSuicide",
|
||||
1308: "C2gUnsubscribe",
|
||||
1309: "C2gSubscribe",
|
||||
3101: "G2cPong",
|
||||
3102: "G2cResult",
|
||||
3103: "G2cChallenge",
|
||||
3104: "G2cSendMessage",
|
||||
3105: "G2cStateChange",
|
||||
3106: "G2cJoinedQueue",
|
||||
3107: "G2cLeftQueue",
|
||||
3108: "G2cCreateMount",
|
||||
3109: "G2cConnectionClosed",
|
||||
}
|
||||
|
||||
// TypeName is a message type's name, or its number if it's unknown.
|
||||
func TypeName(t uint64) string {
|
||||
if name, ok := typeNames[t]; ok {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("type %d", t)
|
||||
}
|
||||
|
||||
// Frame is one message: its type and the size of its data.
|
||||
type Frame struct {
|
||||
Type uint64
|
||||
Size int
|
||||
}
|
||||
|
||||
// ErrMalformed means the stream isn't IMQ framing; a Reader stops decoding after it.
|
||||
var ErrMalformed = errors.New("imq: malformed frame")
|
||||
|
||||
// Reader decodes frames from a stream given in pieces of any size.
|
||||
type Reader struct {
|
||||
buf []byte
|
||||
err error
|
||||
}
|
||||
|
||||
// Write adds data from the stream and returns the frames it completed.
|
||||
func (r *Reader) Write(data []byte) ([]Frame, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
r.buf = append(r.buf, data...)
|
||||
var frames []Frame
|
||||
for {
|
||||
f, n, err := parse(r.buf)
|
||||
if err != nil {
|
||||
r.err, r.buf = err, nil
|
||||
return frames, err
|
||||
}
|
||||
if n == 0 {
|
||||
return frames, nil
|
||||
}
|
||||
frames = append(frames, f)
|
||||
r.buf = r.buf[n:]
|
||||
}
|
||||
}
|
||||
|
||||
// maxFrame bounds a frame's size, so a stream that isn't IMQ isn't buffered without end.
|
||||
const maxFrame = 16 << 20
|
||||
|
||||
// parse reads the frame at the start of b: the frame and its length, or a length of 0 if b holds only part of it.
|
||||
func parse(b []byte) (Frame, int, error) {
|
||||
var f Frame
|
||||
i := 0
|
||||
for i < len(b) {
|
||||
key, n := varint(b[i:])
|
||||
if n == 0 {
|
||||
return f, 0, nil
|
||||
}
|
||||
if n < 0 {
|
||||
return f, 0, ErrMalformed
|
||||
}
|
||||
i += n
|
||||
switch key {
|
||||
case 1<<3 | 0: // field 1, varint: the type
|
||||
v, n := varint(b[i:])
|
||||
if n == 0 {
|
||||
return f, 0, nil
|
||||
}
|
||||
if n < 0 {
|
||||
return f, 0, ErrMalformed
|
||||
}
|
||||
f.Type = v
|
||||
i += n
|
||||
case 2<<3 | 2: // field 2, bytes: the message, the frame's last field
|
||||
size, n := varint(b[i:])
|
||||
if n == 0 {
|
||||
return f, 0, nil
|
||||
}
|
||||
if n < 0 || size > maxFrame {
|
||||
return f, 0, ErrMalformed
|
||||
}
|
||||
i += n
|
||||
if len(b)-i < int(size) {
|
||||
return f, 0, nil
|
||||
}
|
||||
f.Size = int(size)
|
||||
return f, i + int(size), nil
|
||||
default:
|
||||
return f, 0, ErrMalformed
|
||||
}
|
||||
}
|
||||
return f, 0, nil
|
||||
}
|
||||
|
||||
// varint decodes a protobuf varint: its value and length, a length of 0 if b ends first, or -1 if it's too long.
|
||||
func varint(b []byte) (uint64, int) {
|
||||
var v uint64
|
||||
for i := 0; i < len(b); i++ {
|
||||
if i == 10 {
|
||||
return 0, -1
|
||||
}
|
||||
v |= uint64(b[i]&0x7f) << (7 * i)
|
||||
if b[i]&0x80 == 0 {
|
||||
return v, i + 1
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package imq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// frame encodes a frame as the client does: field 1 the type, field 2 the data.
|
||||
func frame(t uint64, data []byte) []byte {
|
||||
b := append([]byte{0x08}, uvarint(t)...)
|
||||
b = append(b, 0x12)
|
||||
b = append(b, uvarint(uint64(len(data)))...)
|
||||
return append(b, data...)
|
||||
}
|
||||
|
||||
func uvarint(v uint64) []byte {
|
||||
var b []byte
|
||||
for v >= 0x80 {
|
||||
b = append(b, byte(v)|0x80)
|
||||
v >>= 7
|
||||
}
|
||||
return append(b, byte(v))
|
||||
}
|
||||
|
||||
func TestReaderSplitsFrames(t *testing.T) {
|
||||
stream := append(frame(1302, []byte("hello")), frame(3103, bytes.Repeat([]byte{1}, 300))...)
|
||||
stream = append(stream, frame(1301, nil)...)
|
||||
// Fed a byte at a time, the frames still come out whole and in order.
|
||||
var r Reader
|
||||
var got []Frame
|
||||
for i := range stream {
|
||||
frames, err := r.Write(stream[i : i+1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = append(got, frames...)
|
||||
}
|
||||
want := []Frame{{1302, 5}, {3103, 300}, {1301, 0}}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaderRejectsOtherStreams(t *testing.T) {
|
||||
var r Reader
|
||||
// A TLS ClientHello isn't IMQ.
|
||||
if _, err := r.Write([]byte{0x16, 0x03, 0x01, 0x00, 0xa5}); !errors.Is(err, ErrMalformed) {
|
||||
t.Fatalf("got %v, want ErrMalformed", err)
|
||||
}
|
||||
if _, err := r.Write(frame(1301, nil)); !errors.Is(err, ErrMalformed) {
|
||||
t.Fatal("a reader keeps failing after a malformed frame")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeName(t *testing.T) {
|
||||
if TypeName(3104) != "G2cSendMessage" || TypeName(7) != "type 7" {
|
||||
t.Fatal(TypeName(3104), TypeName(7))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user