-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
365 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,350 @@ | ||
import type { Buffer } from 'node:buffer' | ||
import type { HeaderRecord } from './header' | ||
import { isOdd } from './helpers' | ||
|
||
export const AUTH_START_ACTIONS = { | ||
TAC_PLUS_AUTHEN_LOGIN: 0x01, | ||
TAC_PLUS_AUTHEN_CHPASS: 0x02, | ||
TAC_PLUS_AUTHEN_SENDAUTH: 0x04, | ||
} as const | ||
|
||
const ALLOWED_AUTH_START_ACTIONS_VALUES = Object.values(AUTH_START_ACTIONS) | ||
|
||
type AuthStartAction = typeof ALLOWED_AUTH_START_ACTIONS_VALUES[number] | ||
|
||
function isAuthStartAction(maybeAction: number): maybeAction is AuthStartAction { | ||
return (ALLOWED_AUTH_START_ACTIONS_VALUES as number[]).includes(maybeAction) | ||
} | ||
|
||
// Used privileges | ||
// interface PrivilegeLevels { | ||
// TAC_PLUS_PRIV_LVL_MIN: 0x00 | ||
// TAC_PLUS_PRIV_LVL_USER: 0x01 | ||
// TAC_PLUS_PRIV_LVL_ROOT: 0x0F | ||
// TAC_PLUS_PRIV_LVL_MAX: 0x0F | ||
// } | ||
|
||
type PrivLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ||
|
||
function isPrivLevel(maybePrivLevel: number): maybePrivLevel is PrivLevel { | ||
return maybePrivLevel > -1 && maybePrivLevel < 16 | ||
} | ||
|
||
export const AUTHEN_TYPES = { | ||
TAC_PLUS_AUTHEN_TYPE_ASCII: 0x01, | ||
TAC_PLUS_AUTHEN_TYPE_PAP: 0x02, | ||
TAC_PLUS_AUTHEN_TYPE_CHAP: 0x03, | ||
TAC_PLUS_AUTHEN_TYPE_MSCHAP: 0x05, | ||
TAC_PLUS_AUTHEN_TYPE_MSCHAPV2: 0x06, | ||
} as const | ||
|
||
const ALLOWED_AUTHEN_TYPES = Object.values(AUTHEN_TYPES) | ||
|
||
type AuthenType = typeof ALLOWED_AUTHEN_TYPES[number] | ||
|
||
function isAuthenType(maybeAuthenType: number): maybeAuthenType is AuthenType { | ||
return (ALLOWED_AUTHEN_TYPES as number[]).includes(maybeAuthenType) | ||
} | ||
|
||
export const AUTHEN_SERVICE = { | ||
TAC_PLUS_AUTHEN_SVC_NONE: 0x00, | ||
TAC_PLUS_AUTHEN_SVC_LOGIN: 0x01, | ||
TAC_PLUS_AUTHEN_SVC_ENABLE: 0x02, | ||
TAC_PLUS_AUTHEN_SVC_PPP: 0x03, | ||
TAC_PLUS_AUTHEN_SVC_PT: 0x05, | ||
TAC_PLUS_AUTHEN_SVC_RCMD: 0x06, | ||
TAC_PLUS_AUTHEN_SVC_X25: 0x07, | ||
TAC_PLUS_AUTHEN_SVC_NASI: 0x08, | ||
TAC_PLUS_AUTHEN_SVC_FWPROXY: 0x09, | ||
} as const | ||
|
||
const ALLOWED_AUTHEN_SERVICE = Object.values(AUTHEN_SERVICE) | ||
|
||
type AuthenService = typeof ALLOWED_AUTHEN_SERVICE[number] | ||
|
||
function isAuthenService(maybeAuthenService: number): maybeAuthenService is AuthenService { | ||
return (ALLOWED_AUTHEN_SERVICE as number[]).includes(maybeAuthenService) | ||
} | ||
|
||
interface AuthStartRecord { | ||
action: AuthStartAction | ||
privLvl: PrivLevel | ||
authenType: AuthenType | ||
authenService: AuthenService | ||
userLen: number | ||
user: string | null | ||
portLen: number | ||
port: string | null | ||
remAddrLen: number | ||
remAddr: string | null | ||
dataLen: number | ||
content: Buffer | null | ||
} | ||
|
||
type UnknownAuthStart = Record<'action' | 'privLvl' | 'authenType' | 'authenService' | 'userLen' | 'portLen' | 'remAddrLen' | 'dataLen', number> | ||
|
||
function validateAuthStart({ action, privLvl, authenType, authenService, userLen, portLen, remAddrLen, dataLen }: UnknownAuthStart) { | ||
if (!isAuthStartAction(action)) { | ||
throw new Error('Invalid action') | ||
} | ||
|
||
if (!isPrivLevel(privLvl)) { | ||
throw new Error('Invalid privilege level') | ||
} | ||
|
||
if (!isAuthenType(authenType)) { | ||
throw new Error('Invalid authentication type') | ||
} | ||
|
||
if (!isAuthenService(authenService)) { | ||
throw new Error('Invalid authentication service') | ||
} | ||
|
||
return { | ||
action, | ||
privLvl, | ||
authenType, | ||
authenService, | ||
userLen, | ||
portLen, | ||
remAddrLen, | ||
dataLen, | ||
} | ||
} | ||
|
||
export const STATUSES = { | ||
TAC_PLUS_AUTHEN_STATUS_PASS: 0x01, | ||
TAC_PLUS_AUTHEN_STATUS_FAIL: 0x02, | ||
TAC_PLUS_AUTHEN_STATUS_GETDATA: 0x03, | ||
TAC_PLUS_AUTHEN_STATUS_GETUSER: 0x04, | ||
TAC_PLUS_AUTHEN_STATUS_GETPASS: 0x05, | ||
TAC_PLUS_AUTHEN_STATUS_RESTART: 0x06, | ||
TAC_PLUS_AUTHEN_STATUS_ERROR: 0x07, | ||
TAC_PLUS_AUTHEN_STATUS_FOLLOW: 0x21, | ||
} as const | ||
|
||
const ALLOWED_STATUSES = Object.values(STATUSES) | ||
|
||
type Status = typeof ALLOWED_STATUSES[number] | ||
|
||
function isStatus(maybeStatus: number): maybeStatus is Status { | ||
return (ALLOWED_STATUSES as number[]).includes(maybeStatus) | ||
} | ||
|
||
export const AUTH_REPLY_FLAGS = { | ||
TAC_PLUS_REPLY_FLAG_NOECHO: 0x01, | ||
} as const | ||
|
||
const ALLOWED_AUTH_REPLY_FLAGS = Object.values(AUTH_REPLY_FLAGS) | ||
|
||
type ReplyFlag = typeof ALLOWED_AUTH_REPLY_FLAGS[number] | ||
|
||
function isReplyFlag(maybeReplyFlag: number): maybeReplyFlag is ReplyFlag { | ||
return (ALLOWED_AUTH_REPLY_FLAGS as number[]).includes(maybeReplyFlag) | ||
} | ||
|
||
interface AuthReplyRecord { | ||
status: Status | ||
flags: ReplyFlag | ||
messageLength: number | ||
contentLength: number | ||
content: string | null | ||
message: string | null | ||
} | ||
|
||
type UnknownAuthReply = Record<'status' | 'flags' | 'messageLength' | 'contentLength', number> | ||
|
||
function validateAuthReply({ status, flags, messageLength, contentLength }: UnknownAuthReply) { | ||
if (!isStatus(status)) { | ||
throw new Error('Invalid status') | ||
} | ||
|
||
if (!isReplyFlag(flags)) { | ||
throw new Error('Invalid reply flag') | ||
} | ||
|
||
return { | ||
status, | ||
flags, | ||
messageLength, | ||
contentLength, | ||
} | ||
} | ||
|
||
export const AUTH_CONTINUE_FLAGS = { | ||
TAC_PLUS_CONTINUE_FLAG_ABORT: 0x01, | ||
} as const | ||
|
||
const ALLOWED_AUTH_CONTINUE_FLAGS = Object.values(AUTH_CONTINUE_FLAGS) | ||
|
||
type ContinueFlag = typeof ALLOWED_AUTH_CONTINUE_FLAGS[number] | ||
|
||
function isContinueFlag(maybeContinueFlag: number): maybeContinueFlag is ContinueFlag { | ||
return (ALLOWED_AUTH_CONTINUE_FLAGS as number[]).includes(maybeContinueFlag) | ||
} | ||
|
||
interface AuthContinueRecord { | ||
userMessageLength: number | ||
dataLength: number | ||
flags: ContinueFlag | ||
userMessage: string | null | ||
content: string | null | ||
} | ||
|
||
type UnknownAuthContinue = Record<'userMessageLength' | 'dataLength' | 'flags', number> | ||
|
||
function validateAuthContinue({ userMessageLength, dataLength, flags }: UnknownAuthContinue) { | ||
if (!isContinueFlag(flags)) { | ||
throw new Error('Invalid continue flag') | ||
} | ||
|
||
return { | ||
userMessageLength, | ||
dataLength, | ||
flags, | ||
} | ||
} | ||
|
||
export class Authentication { | ||
static decodeAuthStart(data: Buffer, length: HeaderRecord['length']): AuthStartRecord { | ||
if (data.length < Authentication.START_MIN_LENGTH) { | ||
throw new Error('Invalid auth start length') | ||
} | ||
|
||
const authStart = validateAuthStart({ | ||
action: data.readUInt8(0), | ||
privLvl: data.readUInt8(1), | ||
authenType: data.readUInt8(2), | ||
authenService: data.readUInt8(3), | ||
userLen: data.readUInt8(4), | ||
portLen: data.readUInt8(5), | ||
remAddrLen: data.readUInt8(6), | ||
dataLen: data.readUInt8(7), | ||
}) | ||
|
||
let currentPosition = 8 | ||
let user = null | ||
let port = null | ||
let remAddr = null | ||
let content = null | ||
|
||
if (authStart.userLen > 0) { | ||
user = data.subarray(currentPosition, currentPosition + authStart.userLen).toString('utf8') | ||
currentPosition += authStart.userLen | ||
} | ||
|
||
if (authStart.portLen > 0) { | ||
port = data.subarray(currentPosition, currentPosition + authStart.portLen).toString('ascii') | ||
currentPosition += authStart.portLen | ||
} | ||
|
||
if (authStart.remAddrLen > 0) { | ||
remAddr = data.subarray(currentPosition, currentPosition + authStart.remAddrLen).toString('ascii') | ||
currentPosition += authStart.remAddrLen | ||
} | ||
|
||
if (authStart.dataLen > 0) { | ||
content = data.subarray(currentPosition, currentPosition + authStart.dataLen) | ||
currentPosition += authStart.dataLen | ||
} | ||
|
||
if (currentPosition !== length) { | ||
throw new Error('Incorrect length in header') | ||
} | ||
|
||
return { | ||
...authStart, | ||
user, | ||
port, | ||
remAddr, | ||
content, | ||
} | ||
} | ||
|
||
static decodeAuthReply(data: Buffer, length: HeaderRecord['length']): AuthReplyRecord { | ||
if (data.length < Authentication.REPLY_MIN_LENGTH) { | ||
throw new Error('Invalid reply header length') | ||
} | ||
|
||
const authReply = validateAuthReply({ | ||
status: (data.readUInt8(0) & 0xF), | ||
flags: (data.readUInt8(1) & 0xF), | ||
messageLength: data.readUInt16BE(2), | ||
contentLength: data.readUInt16BE(4), | ||
}) | ||
|
||
let message = null | ||
let content = null | ||
let pos = 6 | ||
|
||
if (authReply.messageLength > 0) { | ||
message = data.subarray(pos, pos + authReply.messageLength).toString('ascii') | ||
pos += authReply.messageLength | ||
} | ||
|
||
if (authReply.contentLength > 0) { | ||
content = data.subarray(pos, pos + authReply.contentLength).toString('utf8') | ||
pos += authReply.contentLength | ||
} | ||
|
||
if (pos !== length) { | ||
throw new Error('Incorrect length in header') | ||
} | ||
|
||
return { | ||
...authReply, | ||
message, | ||
content, | ||
} | ||
} | ||
|
||
static decodeAuthContinue(data: Buffer, length: HeaderRecord['length']): AuthContinueRecord { | ||
if (data.length < Authentication.CONTINUE_MIN_LENGTH) { | ||
throw new Error('Invalid continue header length') | ||
} | ||
|
||
const authContinue = validateAuthContinue({ | ||
userMessageLength: data.readUInt16BE(0), | ||
dataLength: data.readUInt16BE(2), | ||
flags: data.readUInt8(4), | ||
}) | ||
|
||
const userMessage = authContinue.userMessageLength > 0 | ||
? data.subarray(5, 5 + authContinue.userMessageLength).toString('ascii') | ||
: null | ||
|
||
const content = authContinue.dataLength > 0 | ||
? data.subarray(5 + authContinue.userMessageLength, 5 + authContinue.userMessageLength + authContinue.dataLength).toString('utf8') | ||
: null | ||
|
||
const pos = 5 + authContinue.userMessageLength + authContinue.dataLength | ||
|
||
if (pos !== length) { | ||
throw new Error('Incorrect length in header') | ||
} | ||
|
||
return { | ||
...authContinue, | ||
userMessage, | ||
content, | ||
} | ||
} | ||
|
||
static handleAuthentication(data: Buffer, header: HeaderRecord) { | ||
const shouldAuthContinue = isOdd(header.seqNo) | ||
|
||
if (header.seqNo === 1) { | ||
return Authentication.decodeAuthStart(data, header.length) | ||
} | ||
|
||
if (shouldAuthContinue) { | ||
return Authentication.decodeAuthContinue(data, header.length) | ||
} | ||
|
||
return Authentication.decodeAuthReply(data, header.length) | ||
} | ||
|
||
static readonly START_MIN_LENGTH = 8 | ||
static readonly REPLY_MIN_LENGTH = 6 | ||
static readonly CONTINUE_MIN_LENGTH = 5 | ||
} |
Oops, something went wrong.