T5DE's local proxy for IMVU Classic
CI / build (push) Successful in 45s

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:
dhkatzandClaude Opus 5.5 committed 2026-10-29 00:03:51 -07:00
commit 2551888ed6
19 files changed
+1498

No files matched your search

+140
View File
@@ -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
}
+65
View File
@@ -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))
}
}