Skip to content

Commit

Permalink
feat: add authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
LwveMike committed Apr 16, 2024
1 parent 6458e11 commit 422c212
Show file tree
Hide file tree
Showing 2 changed files with 365 additions and 7 deletions.
350 changes: 350 additions & 0 deletions src/authentication.ts
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
}
Loading

0 comments on commit 422c212

Please sign in to comment.