Initial commit
This commit is contained in:
66
src/server/socket/console/socket.ts
Normal file
66
src/server/socket/console/socket.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import Logging, { LoggingListeners } from "@proxnet/undead-logging";
|
||||
import { ConsoleEvent } from "./types.ts";
|
||||
import { type ConsoleItem, ConsoleItemSchema } from "./zod.ts";
|
||||
import Server from "../../server.ts";
|
||||
import { getSourceAddress } from "../../../util/net.ts";
|
||||
import { consoleSockets } from "../../../main.ts";
|
||||
import chalk from "npm:chalk@^5.3.0";
|
||||
|
||||
export default class SocketConsoleHandler {
|
||||
|
||||
#socket: WebSocket;
|
||||
#log = new Logging('Console');
|
||||
|
||||
#logCb = (msg: string) => {
|
||||
this.send(ConsoleEvent.Message, msg);
|
||||
}
|
||||
|
||||
constructor(socket: WebSocket, req: Request, serveInfo: Deno.ServeHandlerInfo<Deno.NetAddr>) {
|
||||
this.#socket = socket;
|
||||
|
||||
this.#socket.onmessage = ev => {
|
||||
this.#onMsg(ev);
|
||||
};
|
||||
this.#socket.onerror = () => {
|
||||
this.#log.e(`Socket from ${getSourceAddress(req, serveInfo.remoteAddr)} closed due to error`);
|
||||
this.destroy();
|
||||
}
|
||||
this.#socket.onclose = () => {
|
||||
this.#log.n(`${getSourceAddress(req, serveInfo.remoteAddr)} closed a socket`);
|
||||
this.destroy();
|
||||
}
|
||||
this.#socket.onopen = () => {
|
||||
this.#log.n(`${getSourceAddress(req, serveInfo.remoteAddr)} opened a socket`);
|
||||
}
|
||||
LoggingListeners.onmsg('basic', this.#logCb);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
LoggingListeners.offmsg('basic', this.#logCb);
|
||||
consoleSockets.delete(this);
|
||||
this.#socket.close();
|
||||
}
|
||||
|
||||
send(ev: ConsoleEvent, d: string) {
|
||||
if (ev == ConsoleEvent.Command) this.#socket.send(JSON.stringify({ e: ev, d }));
|
||||
else if (ev == ConsoleEvent.Message) this.#socket.send(JSON.stringify({ e: ev, m: d }));
|
||||
else if (ev == ConsoleEvent.Close) this.#socket.send(JSON.stringify({ e: ev }));
|
||||
}
|
||||
|
||||
async #onMsg(ev: MessageEvent) {
|
||||
try {
|
||||
const parsed = JSON.parse(ev.data) as ConsoleItem;
|
||||
const zodd = ConsoleItemSchema.safeParse(parsed);
|
||||
if (!zodd.success) this.destroy();
|
||||
else if (zodd.data.e == ConsoleEvent.Command) {
|
||||
const data = await Server.Commands.dispatch(...zodd.data.d.split(' '));
|
||||
if (data instanceof Error) throw data;
|
||||
this.send(ConsoleEvent.Message, chalk.gray(`> ${chalk.yellow(data)}`));
|
||||
}
|
||||
else if (zodd.data.e == ConsoleEvent.Close) this.destroy();
|
||||
} catch (err) {
|
||||
this.#log.e(err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
5
src/server/socket/console/types.ts
Normal file
5
src/server/socket/console/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum ConsoleEvent {
|
||||
Message,
|
||||
Command,
|
||||
Close
|
||||
}
|
||||
25
src/server/socket/console/zod.ts
Normal file
25
src/server/socket/console/zod.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import z from "zod";
|
||||
import { ConsoleEvent } from "./types.ts";
|
||||
|
||||
export const ConsoleCommandSchema = z.object({
|
||||
e: z.literal(ConsoleEvent.Command),
|
||||
d: z.string()
|
||||
});
|
||||
|
||||
export const ConsoleMessageSchema = z.object({
|
||||
e: z.literal(ConsoleEvent.Message),
|
||||
m: z.string()
|
||||
});
|
||||
|
||||
export const ConsoleCloseSchema = z.object({
|
||||
e: z.literal(ConsoleEvent.Close),
|
||||
r: z.string()
|
||||
});
|
||||
|
||||
export const ConsoleItemSchema = z.discriminatedUnion('e', [
|
||||
ConsoleCommandSchema,
|
||||
ConsoleMessageSchema,
|
||||
ConsoleCloseSchema
|
||||
]);
|
||||
|
||||
export type ConsoleItem = z.infer<typeof ConsoleItemSchema>;
|
||||
176
src/server/socket/signalr/socket.ts
Normal file
176
src/server/socket/signalr/socket.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import Logging from "@proxnet/undead-logging";
|
||||
import {
|
||||
CompletionMessage,
|
||||
Message,
|
||||
MessageKind,
|
||||
PushNotificationId,
|
||||
SignalMessageType,
|
||||
SignalRMessage,
|
||||
SignalRMessageSchema,
|
||||
TargetResult,
|
||||
TargetResultFailure,
|
||||
TargetResultNotATarget,
|
||||
TargetResultSuccess,
|
||||
TargetResultType
|
||||
} from "./types.ts";
|
||||
import { SocketTarget } from "./targets/targetbase.ts";
|
||||
import type Profile from "../../profiles/profile.ts";
|
||||
import { detailedLog } from "../../../main.ts";
|
||||
|
||||
const logmessages = true;
|
||||
|
||||
export class SignalRSocketHandler {
|
||||
|
||||
#log: Logging = new Logging("SignalMock-");
|
||||
|
||||
#socket: WebSocket;
|
||||
#profile: Profile;
|
||||
|
||||
#Targets: Map<string, SocketTarget> = new Map();
|
||||
|
||||
#killed = false;
|
||||
|
||||
constructor(socket: WebSocket, player: Profile) {
|
||||
|
||||
this.#socket = socket;
|
||||
this.#profile = player;
|
||||
|
||||
this.#init();
|
||||
|
||||
player.setSocketHandler(this);
|
||||
|
||||
//this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
||||
|
||||
for (const target of this.#Targets.values()) target.onInit();
|
||||
|
||||
}
|
||||
|
||||
async #dispatchTarget<T = unknown>(target: string, args: unknown): Promise<TargetResult> {
|
||||
if (this.#killed) {
|
||||
const error = "Tried to dispatch socket target on dead socket";
|
||||
this.#log.w(error);
|
||||
return { type: TargetResultType.Failure, err: error };
|
||||
}
|
||||
const targetExec = this.#Targets.get(target);
|
||||
if (!targetExec) return { type: TargetResultType.NotATarget } as TargetResultNotATarget;
|
||||
else {
|
||||
try {
|
||||
return { type: TargetResultType.Success, data: await targetExec.exec(args) } as TargetResultSuccess<T>;
|
||||
} catch (err) {
|
||||
this.#log.w(`Target '${target}' function error: ${err}`);
|
||||
if (err instanceof Error) return { type: TargetResultType.Failure, err: err } as TargetResultFailure;
|
||||
else return { type: TargetResultType.Failure, err: `${err}` } as TargetResultFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #onMessage(message: Message) {
|
||||
if (message.kind == MessageKind.Protocol) {
|
||||
this.sendRaw({});
|
||||
return;
|
||||
} else {
|
||||
if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
|
||||
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
|
||||
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
|
||||
if (res.type == TargetResultType.Success) {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
result: JSON.stringify(res.data)
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
} else if (res.type == TargetResultType.Failure) {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
error: res.err instanceof Error ? res.err.message : res.err
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
} else {
|
||||
const signalRes: CompletionMessage = {
|
||||
type: SignalMessageType.Completion,
|
||||
invocationId: message.data.invocationId,
|
||||
error: "Target not found"
|
||||
}
|
||||
this.sendRaw(signalRes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#init() {
|
||||
this.#log.source += this.#profile.getId().toString();
|
||||
|
||||
this.#log.i(`Created hub socket`);
|
||||
|
||||
this.#socket.addEventListener('message', message => {
|
||||
try {
|
||||
|
||||
const dec = new TextDecoder();
|
||||
const str = dec.decode(message.data);
|
||||
const data = JSON.parse(str.substring(0, str.length - 1));
|
||||
|
||||
const parseResult = SignalRMessageSchema.safeParse(data);
|
||||
if (parseResult.success) this.#onMessage({
|
||||
kind: MessageKind.Data,
|
||||
data: parseResult.data as SignalRMessage
|
||||
});
|
||||
else {
|
||||
this.#onMessage({
|
||||
kind: MessageKind.Protocol
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
this.#log.e(`Socket error: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.#socket.addEventListener('close', this.destroy(this, true));
|
||||
}
|
||||
|
||||
destroy(handler: SignalRSocketHandler, internal: boolean | undefined = false) {
|
||||
return (ev: CloseEvent) => {
|
||||
handler.#killed = true;
|
||||
|
||||
let errorReason = "Socket closed by server";
|
||||
this.#log.d(`Socket close code: ${ev.code}`);
|
||||
if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request";
|
||||
|
||||
handler.sendRaw({ type: 7, error: errorReason });
|
||||
|
||||
if (!internal) handler.#socket.close();
|
||||
handler.#log.i(`Closed socket`);
|
||||
handler.#profile.setSocketHandler(null);
|
||||
|
||||
for (const target of handler.#Targets.values()) target.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
sendRaw(data: object) {
|
||||
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
||||
if (logmessages) {
|
||||
const isHandshake = JSON.stringify(data) == '{}';
|
||||
if (isHandshake) this.#log.d(detailedLog([`SERVER MESSAGE`,
|
||||
`Type: Handshake`
|
||||
]));
|
||||
else this.#log.d(detailedLog([`SERVER MESSAGE`,
|
||||
`Type: ${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`,
|
||||
`Content: ${JSON.stringify(data)}`
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
sendNotification(id: PushNotificationId | string, args?: object) {
|
||||
const msg: SignalRMessage = {
|
||||
type: SignalMessageType.Invocation,
|
||||
target: "Notification",
|
||||
arguments: [JSON.stringify({
|
||||
Id: id,
|
||||
Msg: args ? args : {}
|
||||
})]
|
||||
}
|
||||
this.sendRaw(msg);
|
||||
}
|
||||
|
||||
}
|
||||
41
src/server/socket/signalr/targets/targetbase.ts
Normal file
41
src/server/socket/signalr/targets/targetbase.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import type { SignalRSocketHandler } from "../socket.ts";
|
||||
|
||||
export class SocketTarget {
|
||||
|
||||
socket: SignalRSocketHandler;
|
||||
|
||||
constructor(socket: SignalRSocketHandler) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
onInit() {
|
||||
return;
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
return;
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async exec(_args: unknown) {
|
||||
throw new Error("Execution for this target is not set.");
|
||||
}
|
||||
|
||||
}
|
||||
190
src/server/socket/signalr/types.ts
Normal file
190
src/server/socket/signalr/types.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/* Galvanic Corrosion - Rec Room custom server for communities.
|
||||
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
|
||||
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export enum MessageKind {
|
||||
Protocol,
|
||||
Data
|
||||
}
|
||||
export interface MessageBase {
|
||||
kind: MessageKind
|
||||
}
|
||||
export interface DataMessage extends MessageBase {
|
||||
kind: MessageKind.Data,
|
||||
data: SignalRMessage
|
||||
}
|
||||
export interface ProtocolMessage extends MessageBase {
|
||||
kind: MessageKind.Protocol
|
||||
}
|
||||
export type Message = ProtocolMessage | DataMessage;
|
||||
|
||||
export type SignalRMessage =
|
||||
| InvocationMessage
|
||||
| StreamItemMessage
|
||||
| CompletionMessage
|
||||
| PingMessage
|
||||
| CloseMessage;
|
||||
|
||||
export enum SignalMessageType {
|
||||
Handshake,
|
||||
Invocation,
|
||||
StreamItem,
|
||||
Completion,
|
||||
StreamInvocation,
|
||||
CancelInvocation,
|
||||
Ping,
|
||||
Close
|
||||
}
|
||||
|
||||
export interface BaseMessage {
|
||||
type: SignalMessageType;
|
||||
}
|
||||
|
||||
export interface InvocationMessage extends BaseMessage {
|
||||
type: SignalMessageType.Invocation;
|
||||
target: string;
|
||||
arguments: unknown[];
|
||||
invocationId?: string;
|
||||
}
|
||||
|
||||
export interface StreamItemMessage extends BaseMessage {
|
||||
type: SignalMessageType.StreamItem;
|
||||
invocationId: string;
|
||||
item: unknown;
|
||||
}
|
||||
|
||||
export interface CompletionMessage extends BaseMessage {
|
||||
type: SignalMessageType.Completion;
|
||||
invocationId: string;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface PingMessage extends BaseMessage {
|
||||
type: SignalMessageType.Ping;
|
||||
}
|
||||
|
||||
export interface CloseMessage extends BaseMessage {
|
||||
type: SignalMessageType.Close;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const BaseMessageSchema = z.object({
|
||||
type: z.enum(SignalMessageType),
|
||||
});
|
||||
|
||||
export const InvocationMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Invocation),
|
||||
target: z.string(),
|
||||
arguments: z.array(z.unknown()),
|
||||
invocationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const StreamItemMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.StreamItem),
|
||||
invocationId: z.string(),
|
||||
item: z.unknown(),
|
||||
});
|
||||
|
||||
export const CompletionMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Completion),
|
||||
invocationId: z.string(),
|
||||
result: z.unknown().optional(),
|
||||
error: z.string().optional(),
|
||||
});
|
||||
|
||||
export const PingMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Ping),
|
||||
});
|
||||
|
||||
export const CloseMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal(SignalMessageType.Close),
|
||||
error: z.optional(z.string()),
|
||||
});
|
||||
|
||||
export const SignalRMessageSchema = z.discriminatedUnion("type", [
|
||||
InvocationMessageSchema,
|
||||
StreamItemMessageSchema,
|
||||
CompletionMessageSchema,
|
||||
PingMessageSchema,
|
||||
CloseMessageSchema,
|
||||
]);
|
||||
|
||||
export enum TargetResultType {
|
||||
Success,
|
||||
Failure,
|
||||
NotATarget
|
||||
}
|
||||
export interface TargetResultBase {
|
||||
type: TargetResultType
|
||||
}
|
||||
export interface TargetResultSuccess<T = unknown> extends TargetResultBase {
|
||||
type: TargetResultType.Success,
|
||||
data: T
|
||||
}
|
||||
export interface TargetResultFailure extends TargetResultBase {
|
||||
type: TargetResultType.Failure
|
||||
err: string | Error
|
||||
}
|
||||
export interface TargetResultNotATarget extends TargetResultBase {
|
||||
type: TargetResultType.NotATarget
|
||||
}
|
||||
export type TargetResult = TargetResultSuccess | TargetResultFailure | TargetResultNotATarget;
|
||||
|
||||
export enum PushNotificationId {
|
||||
RelationshipChanged = 1,
|
||||
MessageReceived,
|
||||
MessageDeleted,
|
||||
PresenceHeartbeatResponse,
|
||||
RefreshLogin,
|
||||
Logout,
|
||||
SubscriptionUpdateProfile = 11,
|
||||
SubscriptionUpdatePresence,
|
||||
SubscriptionUpdateGameSession,
|
||||
SubscriptionUpdateRoom = 15,
|
||||
SubscriptionUpdateRoomPlaylist,
|
||||
ModerationQuitGame = 20,
|
||||
ModerationUpdateRequired,
|
||||
ModerationKick,
|
||||
ModerationKickAttemptFailed,
|
||||
ModerationRoomBan,
|
||||
ServerMaintenance,
|
||||
GiftPackageReceived = 30,
|
||||
GiftPackageReceivedImmediate,
|
||||
GiftPackageRewardSelectionReceived,
|
||||
ProfileJuniorStatusUpdate = 40,
|
||||
RelationshipsInvalid = 50,
|
||||
StorefrontBalanceAdd = 60,
|
||||
StorefrontBalanceUpdate,
|
||||
StorefrontBalancePurchase,
|
||||
ConsumableMappingAdded = 70,
|
||||
ConsumableMappingRemoved,
|
||||
PlayerEventCreated = 80,
|
||||
PlayerEventUpdated,
|
||||
PlayerEventDeleted,
|
||||
PlayerEventResponseChanged,
|
||||
PlayerEventResponseDeleted,
|
||||
PlayerEventStateChanged,
|
||||
ChatMessageReceived = 90,
|
||||
CommunityBoardUpdate = 95,
|
||||
CommunityBoardAnnouncementUpdate,
|
||||
InventionModerationStateChanged = 100,
|
||||
FreeGiftButtonItemsAdded = 110,
|
||||
LocalRoomKeyCreated = 120,
|
||||
LocalRoomKeyDeleted
|
||||
}
|
||||
Reference in New Issue
Block a user