continuing setting up services

This commit is contained in:
Artem Titoulenko 2021-09-13 15:59:10 -04:00
parent 1fa6d06f7a
commit aad0acfd15
13 changed files with 470 additions and 64 deletions

View file

@ -22,20 +22,24 @@ import AuthorizationRegistrationService from "./services/0x17-AuthorizationRegis
import BaseService from "./services/base";
export interface User {
uin: string,
password: string,
memberSince: Date,
}
export default class Communicator {
private _sequenceNumber = 0;
private messageBuffer = Buffer.alloc(0);
public services : {[key: number]: BaseService} = {};
public user? : User;
constructor(public socket : net.Socket) {
// Hold on to the socket
this.socket = socket;
this.socket.on('data', (data : Buffer) => {
console.log('DATA-----------------------');
console.log('RAW\n' + logDataStream(data));
// we could get multiple FLAP messages, keep a running buffer of incoming
// data and shift-off however many successful FLAPs we can make
this.messageBuffer = Buffer.concat([this.messageBuffer, data]);
@ -43,9 +47,12 @@ export default class Communicator {
while (this.messageBuffer.length > 0) {
try {
const flap = FLAP.fromBuffer(this.messageBuffer);
console.log('DATA-----------------------');
console.log('RAW\n' + logDataStream(flap.toBuffer()));
console.log('RECV', flap.toString());
this.messageBuffer = this.messageBuffer.slice(flap.length);
this.handleMessage(flap);
console.log('-----------------------DATA');
} catch (e) {
// Couldn't make a FLAP
break;
@ -65,7 +72,6 @@ export default class Communicator {
registerServices() {
const services = [
new AuthorizationRegistrationService(this),
new GenericServiceControls(this),
new LocationServices(this),
new BuddyListManagement(this),
@ -80,7 +86,8 @@ export default class Communicator {
new Chat(this),
new DirectorySearch(this),
new ServerStoredBuddyIcons(this),
new SSI(this),
// new SSI(this),
new AuthorizationRegistrationService(this),
];
// Make a map of the service number to the service handler
@ -97,7 +104,6 @@ export default class Communicator {
send(message : FLAP) {
console.log('SEND', message.toString());
console.log('RAW\n' + logDataStream(message.toBuffer()));
console.log('-----------------------DATA');
this.socket.write(message.toBuffer());
}

View file

@ -1,2 +1,64 @@
export const AIM_MD5_STRING = "AOL Instant Messenger (SM)";
export const FLAGS_EMPTY = Buffer.from([0x00, 0x00, 0x00, 0x00]);
export const enum USER_STATUS_VARIOUS {
/**
* Status webaware flag
*/
WEBAWARE = 0x0001,
/**
* Status show ip flag
*/
SHOWIP = 0x0002,
/**
* User birthday flag
*/
BIRTHDAY = 0x0008,
/**
* User active webfront flag
*/
WEBFRONT = 0x0020,
/**
* Direct connection not supported
*/
DCDISABLED = 0x0100,
/**
* Direct connection upon authorization
*/
DCAUTH = 0x1000,
/**
* DC only with contact users
*/
DCCONT = 0x2000,
}
export const enum USER_STATUS {
/**
* Status is online
*/
ONLINE = 0x0000,
/**
* Status is away
*/
AWAY = 0x0001,
/**
* Status is no not disturb (DND)
*/
DND = 0x0002,
/**
* Status is not available (N/A)
*/
NA = 0x0004,
/**
* Status is occupied (BISY)
*/
OCCUPIED = 0x0010,
/**
* Status is free for chat
*/
FREE4CHAT = 0x0020,
/**
* Status is invisible
*/
INVISIBLE = 0x0100,
}

View file

@ -1,9 +1,13 @@
import BaseService from './base';
import Communicator from '../communicator';
import { FLAP, Rate, RateClass, RatedServiceGroup, RateGroupPair, SNAC } from '../structures';
import { FLAGS_EMPTY } from '../consts';
import { FLAP, Rate, RateClass, RatedServiceGroup, RateGroupPair, SNAC, TLV } from '../structures';
import { FLAGS_EMPTY, USER_STATUS_VARIOUS, USER_STATUS } from '../consts';
import { char, word, dword, dot2num } from '../structures/bytes';
export default class GenericServiceControls extends BaseService {
private allowViewIdle = false;
private allowViewMemberSince = false;
constructor(communicator : Communicator) {
super({service: 0x01, version: 0x03}, communicator)
}
@ -13,6 +17,19 @@ export default class GenericServiceControls extends BaseService {
throw new Error('Require SNAC');
}
if (message.payload.subtype === 0x14) {
/*
Client setting privacy settings
Bit 1 - Allows other AIM users to see how long you've been idle.
Bit 2 - Allows other AIM users to see how long you've been a member.
*/
const mask = (message.payload.payload as Buffer).readUInt32BE();
this.allowViewIdle = (mask & 0x01) > 0;
this.allowViewMemberSince = (mask & 0x02) > 0;
console.log('allowViewIdle:', this.allowViewIdle, 'allowViewMemberSince', this.allowViewMemberSince);
return;
}
if (message.payload.subtype === 0x06) { // Client ask server for rate limits info
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
SNAC.forRateClass(0x01, 0x07, FLAGS_EMPTY, 0, [
@ -22,11 +39,56 @@ export default class GenericServiceControls extends BaseService {
)
]))
this.send(resp);
const motd = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x01, 0x13, FLAGS_EMPTY, 0, Buffer.concat([
word(0x0004),
(new TLV(0x0B, Buffer.from("Hello world!"))).toBuffer(),
])))
this.send(motd);
return;
}
if (message.payload.subtype === 0x0e) { // Client requests own online information
console.log('should send back online presence info');
const uin = '400'; // this.communicator.user.uin;
const warning = 0;
const since = +(new Date('December 17, 1998 03:24:00'));
const externalIP = dot2num(this.communicator.socket.remoteAddress!.split(':').pop()!);
const tlvs : TLV[] = [
new TLV(0x01, char(0x80)),
new TLV(0x06, dword(USER_STATUS_VARIOUS.WEBAWARE | USER_STATUS_VARIOUS.DCDISABLED << 2 + USER_STATUS.ONLINE)),
new TLV(0x0A, dword(externalIP)),
new TLV(0x0F, dword(0)), // TODO: track idle time,
new TLV(0x03, dword(Math.floor(Date.now() / 1000))),
new TLV(0x1E, dword(0)), // Unknown
new TLV(0x05, dword(Math.floor(since / 1000))),
new TLV(0x0C, Buffer.concat([
dword(externalIP),
dword(5700), // DC TCP Port
dword(0x04000000), // DC Type,
word(0x0400), // DC Protocol Version
dword(0), // DC Auth Cookie
dword(0), // Web Front port
dword(0x300), // Client Features ?
dword(0), // Last Info Update Time
dword(0), // last EXT info update time,
dword(0), // last ext status update time
]))
];
const payloadHeader = Buffer.alloc(1 + uin.length + 2 + 2);
payloadHeader.writeInt8(uin.length);
payloadHeader.set(Buffer.from(uin), 1);
payloadHeader.writeInt16BE(warning, 1 + uin.length);
payloadHeader.writeInt16BE(tlvs.length, 1 + uin.length + 2);
const buf = Buffer.concat([payloadHeader, ...tlvs.map((tlv) => tlv.toBuffer())])
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x01, 0x0f, FLAGS_EMPTY, 0, buf));
this.send(resp);
return;
}

View file

@ -1,8 +1,37 @@
import BaseService from './base';
import Communicator from '../communicator';
import { FLAP, SNAC, TLV } from '../structures';
import { char, word, dword, dot2num } from '../structures/bytes';
import { FLAGS_EMPTY, USER_STATUS, USER_STATUS_VARIOUS } from '../consts';
export default class LocationServices extends BaseService {
constructor(communicator : Communicator) {
super({service: 0x02, version: 0x01}, communicator)
}
override handleMessage(message : FLAP) {
if (!(message.payload instanceof SNAC)) {
throw new Error('Expecting SNACs for LocationServices')
}
// request location service parameters and limitations
if (message.payload.subtype === 0x02) {
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x02,0x03, FLAGS_EMPTY, 0, [
new TLV(0x01, word(0x400)), // max profile length
new TLV(0x02, word(0x10)), // max capabilities
new TLV(0x03, word(0xA)), // unknown
new TLV(0x04, word(0x1000)),
]));
this.send(resp);
return;
}
if (message.payload.subtype === 0x04) {
// Client use this snac to set its location information (like client
// profile string, client directory profile string, client capabilities).
return;
}
}
}

View file

@ -1,8 +1,30 @@
import BaseService from './base';
import Communicator from '../communicator';
import { FLAGS_EMPTY } from '../consts';
import { FLAP, SNAC, TLV } from '../structures';
import { word } from '../structures/bytes';
export default class BuddyListManagement extends BaseService {
constructor(communicator : Communicator) {
super({service: 0x03, version: 0x01}, communicator)
}
override handleMessage(message : FLAP) {
if (!(message.payload instanceof SNAC)) {
throw new Error('Expected SNACs')
}
if (message.payload.subtype === 0x02) {
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x03, 0x03, FLAGS_EMPTY, 0, [
new TLV(0x01, word(600)), // 600 max buddies
new TLV(0x02, word(750)), // 750 max watchers
new TLV(0x03, word(512)), // 512 max online notifications ?
]));
this.send(resp);
return;
}
}
}

View file

@ -1,8 +1,88 @@
import BaseService from './base';
import Communicator from '../communicator';
import { FLAGS_EMPTY } from '../consts';
import { FLAP, SNAC, TLV } from '../structures';
import { dword } from '../structures/bytes';
interface ChannelSettings {
channel: number,
messageFlags: number,
maxMessageSnacSize: number,
maxSenderWarningLevel: number,
maxReceiverWarningLevel: number,
minimumMessageInterval: number,
unknown : number
}
export default class ICBM extends BaseService {
private channel : ChannelSettings = {
channel: 2,
messageFlags: 3,
maxMessageSnacSize: 512,
maxSenderWarningLevel: 999,
maxReceiverWarningLevel: 999,
minimumMessageInterval: 0,
unknown: 1000,
};
constructor(communicator : Communicator) {
super({service: 0x04, version: 0x01}, communicator)
}
override handleMessage(message : FLAP) {
if (!(message.payload instanceof SNAC)) {
throw new Error('Expected SNACs')
}
if (message.payload.subtype === 0x02) {
// client is telling us about it's ICBM capabilities (whatever)
/*
xx xx word channel to setup
xx xx xx xx dword message flags
xx xx word max message snac size
xx xx word max sender warning level
xx xx word max receiver warning level
xx xx word minimum message interval (sec)
00 00 word unknown parameter (also seen 03 E8)
*/
if (!(message.payload.payload instanceof Buffer)) {
throw new Error('Expected Buffer payload for this SNAC');
}
const payload = message.payload.payload;
this.channel = {
channel: payload.readUInt16BE(0),
messageFlags: payload.readUInt32BE(2),
maxMessageSnacSize: payload.readUInt16BE(6),
maxSenderWarningLevel: payload.readUInt16BE(8),
maxReceiverWarningLevel: payload.readUInt16BE(10),
minimumMessageInterval: payload.readUInt16BE(12),
unknown: payload.readUInt16BE(14),
}
console.log("ICBM set channel", this.channel);
return;
}
if (message.payload.subtype === 0x04) {
const payload = Buffer.alloc(16, 0x00);
payload.writeInt16BE(this.channel.channel, 0);
payload.writeInt32BE(this.channel.messageFlags, 2);
payload.writeInt16BE(this.channel.maxMessageSnacSize, 6);
payload.writeInt16BE(this.channel.maxSenderWarningLevel, 8);
payload.writeInt16BE(this.channel.maxReceiverWarningLevel, 10);
payload.writeInt16BE(this.channel.minimumMessageInterval, 12);
payload.writeInt16BE(this.channel.unknown, 14);
// For some reason this response crashes the client?
// It's identical to the channel set request the client
// sends earlier. Also the 3.x client sends a channel set request
// so early
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x04, 0x05, FLAGS_EMPTY, 0, payload));
this.send(resp);
return;
}
}
}

View file

@ -1,8 +1,38 @@
import BaseService from './base';
import Communicator from '../communicator';
import { FLAGS_EMPTY } from '../consts';
import { FLAP, SNAC, TLV } from '../structures';
import { word } from '../structures/bytes';
export default class PrivacyManagement extends BaseService {
private permissionMask: number = 0xffff; // everyone
constructor(communicator : Communicator) {
super({service: 0x09, version: 0x01}, communicator)
}
override handleMessage(message : FLAP) {
if (!(message.payload instanceof SNAC)) {
throw new Error('Expected SNACs')
}
if (message.payload.subtype === 0x02) {
const resp = new FLAP(0x02, this._getNewSequenceNumber(),
new SNAC(0x09, 0x03, FLAGS_EMPTY, 0, [
new TLV(0x01, word(200)), // max visible list size
new TLV(0x02, word(200)) // max invisible list size
]));
this.send(resp);
return;
}
if (message.payload.subtype === 0x04) {
// Client sends permission mask for classes of users that can talk to the client
this.permissionMask = (message.payload.payload as Buffer).readUInt32BE();
console.log('set permission mask', this.permissionMask.toString(16));
return;
}
}
}

View file

@ -1,12 +1,16 @@
import crypto from 'crypto';
import BaseService from './base';
import Communicator from '../communicator';
import Communicator, { User } from '../communicator';
import { FLAP, SNAC, TLV, ErrorCode, TLVType } from '../structures';
const { AIM_MD5_STRING, FLAGS_EMPTY } = require('../consts');
const users : {[key: string]: string} = {
'toof': 'foo',
const users : {[key: string]: User} = {
'toof': {
uin: '156089',
password: 'foo',
memberSince: new Date('December 17, 1998 03:24:00'),
}
};
export default class AuthorizationRegistrationService extends BaseService {
@ -57,7 +61,7 @@ export default class AuthorizationRegistrationService extends BaseService {
const pwHash = crypto.createHash('md5');
pwHash.update(this.cipher);
pwHash.update(users[username]);
pwHash.update(users[username].password);
pwHash.update(AIM_MD5_STRING);
const digest = pwHash.digest('hex');
@ -72,13 +76,18 @@ export default class AuthorizationRegistrationService extends BaseService {
return;
}
const host = this.communicator.socket.localAddress.split(':').pop();
const port = this.communicator.socket.localPort;
const authResp = new FLAP(2, this._getNewSequenceNumber(),
new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [
TLV.forUsername(username), // username
TLV.forBOSAddress('10.0.1.29:5190'), // BOS address
TLV.forBOSAddress(`${host}:${port}`), // BOS address
TLV.forCookie(JSON.stringify({cookie: 'uwu', user: 'toof'})) // Authorization cookie
]));
this.communicator.user = Object.assign({}, users[username], {username});
this.send(authResp);
return;
case 0x06: // Request md5 authkey

View file

@ -17,7 +17,7 @@ export class FLAP {
let payload : Buffer | SNAC = buf.slice(6, 6 + payloadLength);
if (channel === 2) {
if (channel === 2 && payloadLength > 0) {
payload = SNAC.fromBuffer(payload, payloadLength);
}

View file

@ -78,10 +78,15 @@ export class SNAC {
let payload : Buffer | TLV[]; // SNACs can have multiple payload
// Some SNACs don't have TLV payloads
// Maybe this should be something that the service does itself when it
// wants to respond to a message;
if (service === 0x01 && subtype === 0x17 ||
service === 0x01 && subtype === 0x14 ||
service === 0x01 && subtype === 0x07 ||
service === 0x01 && subtype === 0x08 ||
service === 0x01 && subtype === 0x0e) {
service === 0x01 && subtype === 0x0e ||
service === 0x04 && subtype === 0x02 ||
service === 0x09 && subtype === 0x04) {
payload = buf.slice(10, 10 + payloadLength);
} else {
payload = [];

View file

@ -1,5 +1,8 @@
import {ErrorCode} from "./ErrorCode";
import { USER_STATUS_VARIOUS, USER_STATUS } from "../consts";
import { char, word, dword } from './bytes';
export const enum TLVType {
User = 0x01,
ClientName = 0x03,
@ -36,6 +39,14 @@ export class TLV {
return new TLV(0x08, Buffer.from([0x00, errorCode]));
}
static forStatus(various : USER_STATUS_VARIOUS, status: USER_STATUS) {
const varbuf = Buffer.alloc(2, 0x00);
varbuf.writeUInt16BE(various);
const statbuf = Buffer.alloc(2, 0x00);
statbuf.writeUInt16BE(status);
return new TLV(0x06, Buffer.concat([varbuf, statbuf]));
}
constructor(public type : TLVType, public payload : Buffer) {
this.type = type;
this.payload = payload;
@ -46,9 +57,10 @@ export class TLV {
}
toBuffer() {
const TLVHeader = Buffer.alloc(4, 0, 'hex');
TLVHeader.writeUInt16BE(this.type);
TLVHeader.writeUInt16BE(this.length, 2);
return Buffer.concat([TLVHeader, this.payload]);
return Buffer.concat([
word(this.type),
word(this.length),
this.payload,
]);
}
}

37
src/structures/bytes.ts Normal file
View file

@ -0,0 +1,37 @@
export function char(num : number) : Buffer {
const buf = Buffer.alloc(1, 0x00);
buf.writeUInt8(num);
return buf;
}
export function word(num : number) : Buffer {
const buf = Buffer.alloc(2, 0x00);
buf.writeUInt16BE(num);
return buf;
}
export function dword(num : number) : Buffer {
const buf = Buffer.alloc(4, 0x00);
buf.writeUInt32BE(num);
return buf;
}
/**
* Converts a string IP address to it's number representation.
* From: https://stackoverflow.com/a/8105740
* @param ip IP address string
* @returns IP address as a number
*/
export function dot2num(ip : string) : number {
const d = ip.split('.');
return ((((((+d[0])*256)+(+d[1]))*256)+(+d[2]))*256)+(+d[3]);
}
export function num2dot(num : number) : string {
let d = '' + num%256;
for (var i = 3; i > 0; i--) {
num = Math.floor(num/256);
d = num%256 + '.' + d;
}
return d;
}

View file

@ -1,4 +1,5 @@
import { FLAP } from './structures'
import { dot2num, num2dot } from './structures/bytes'
export function chunkString(str : string, len : number) {
const size = Math.ceil(str.length/len)
const r = Array(size)
@ -26,6 +27,8 @@ interface Spec {
description: string,
size : DataSize,
isRepeat? : boolean,
isParam? : boolean,
isTLV? : boolean,
repeatSpecs?: Spec[],
}
@ -45,34 +48,13 @@ function repeat(size: DataSize, description : string, specs : Spec[]) : Spec {
return {size, description, isRepeat: true, repeatSpecs: specs};
}
const FLAPSpec = [
byte("FLAP Header"),
byte("Channel"),
word("Sequence ID"),
word("Payload Length"),
word("SNAC Family"),
word("SNAC Subtype"),
word("SNAC Flags"),
dword("SNAC Request-ID"),
repeat(16, "Number of rate classes", [
word("Rate Class ID"),
dword("Window Size"),
dword("Clear Level"),
dword("Alert Level"),
dword("Limit Level"),
dword("Disconnect Level"),
dword("Current Level"),
dword("Max Level"),
dword("Last Time"),
byte("Current State")
]),
repeat(-1, "", [
word("Rate Group ID"),
repeat(16, "Number of pairs in group", [
dword("Family/Subtype pair"),
]),
]),
];
function param(size: DataSize, description: string) : Spec {
return {size, description, isParam: true};
}
function tlv(description : string) : Spec {
return {size : -1, description, isTLV: true};
}
function parseBuffer(buf : Buffer, spec : Spec[], repeatTimes = 0) {
let offset = 0;
@ -80,25 +62,50 @@ function parseBuffer(buf : Buffer, spec : Spec[], repeatTimes = 0) {
let repeat = repeatTimes;
for (let section of spec) {
let value : number = 0;
let value : any = 0;
let bufStr : string = '';
if (section.size === 8) {
const bufStr = buf.slice(offset, offset + 1).toString('hex');
bufStr = buf.slice(offset, offset + 1).toString('hex');
value = buf.readInt8(offset);
rows.push([chunkString(bufStr, 2).join(' '), value, section.description]);
offset += 1;
} else if (section.size === 16) {
const bufStr = buf.slice(offset, offset + 2).toString('hex');
bufStr = buf.slice(offset, offset + 2).toString('hex');
value = buf.readUInt16BE(offset);
rows.push([chunkString(bufStr, 2).join(' '), value, section.description]);
offset += 2;
} else if (section.size === 32) {
const bufStr = buf.slice(offset, offset + 4).toString('hex');
bufStr = buf.slice(offset, offset + 4).toString('hex');
value = buf.readUInt32BE(offset);
rows.push([chunkString(bufStr, 2).join(' '), value, section.description]);
offset += 4;
}
if (section.isRepeat && section.repeatSpecs) {
if (section.description.includes("IP")) {
value = num2dot(value);
}
if (section.isParam) {
const paramBuf = buf.slice(offset, offset + value);
offset += value;
rows.push([chunkString(paramBuf.toString('hex'), 2), paramBuf.toString('ascii'), section.description])
} else if (section.isTLV) {
const tlvType = buf.slice(offset, offset + 2).toString('hex');
offset += 2;
const tlvLength = buf.slice(offset, offset + 2).readUInt16BE(0);
offset += 2;
const tlvData = buf.slice(offset, offset + tlvLength);
offset += tlvLength;
let data = tlvData.toString('ascii') as string;
if (section.description.includes("IP")) {
data = num2dot(tlvData.readUInt32BE(0));
}
rows.push([
chunkString(tlvData.toString('hex'), 2),
data,
tlvType + ':' + section.description,
]);
} else if (section.isRepeat && section.repeatSpecs) {
if (section.size !== -1) {
repeat = value;
}
@ -110,6 +117,8 @@ function parseBuffer(buf : Buffer, spec : Spec[], repeatTimes = 0) {
const subrows : any[] = parseBuffer(buf.slice(offset), specs, repeat);
rows.push(...subrows);
} else {
rows.push([chunkString(bufStr, 2).join(' '), value, section.description]);
}
}
@ -125,16 +134,59 @@ function bufferFromWebText(webtext : string) : Buffer {
return Buffer.from(webtext.replace(/\s/g, ''), 'hex');
}
const FLAPSpec = [
byte("FLAP Header"),
byte("Channel"),
word("Sequence ID"),
word("Payload Length"),
word("SNAC Family"),
word("SNAC Subtype"),
word("SNAC Flags"),
dword("SNAC Request-ID"),
param(8, "UIN String Length"),
word("Warning Level"),
word("Number of TLV in list"),
tlv("User Class"),
tlv("User Status"),
tlv("External IP Address"),
tlv("Client Idle Time"),
tlv("Signon Time"),
tlv("Unknown Value"),
tlv("Member Since"),
word("DC Info"),
word("DC Info Length"),
dword("DC Internal IP Address"),
dword("DC TCP Port"),
dword("DC Type"),
word("DC Protocol Version"),
dword("DC Auth Cookie"),
dword("Web Front Port"),
dword("Client Features"),
dword("Last Info Update Time"),
dword("Last EXT info update time"),
dword("Last EXT status update time"),
];
const exampleWebText = ''+
`
2a 02 00 03 00 37 00 01
00 07 00 00 00 00 00 00
00 01 00 01 00 00 00 50
00 00 09 c4 00 00 07 d0
00 00 05 dc 00 00 03 20
00 00 0d 48 00 00 17 70
00 00 00 00 00 00 01 00
01 00 00 00 00 `
2a 02 00 05 00 71 00 01
00 0f 00 00 00 00 00 00
03 34 30 30 00 00 00 08
00 01 00 01 80 00 06 00
04 00 00 04 01 00 0a 00
04 c0 a8 01 fe 00 0f 00
04 00 00 00 00 00 03 00
04 61 3f aa 53 00 1e 00
04 00 00 00 00 00 05 00
04 36 78 bf a0 00 0c 00
26 c0 a8 01 fe 00 00 16
44 04 00 00 00 04 00 00
00 00 00 00 00 00 00 00
00 03 00 00 00 00 00 00
00 00 00 00 00 00 00
`
if (require.main === module) {
printBuffer(bufferFromWebText(exampleWebText), FLAPSpec);