Skip to content

Commit

Permalink
feat, refactor: basic safeguarded message parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
EagleoutIce committed Aug 30, 2023
1 parent 4ea5def commit 5b03a36
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 25 deletions.
Empty file removed src/cli/repl/server/cache.ts
Empty file.
30 changes: 17 additions & 13 deletions src/cli/repl/server/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RShell, TokenMap } from '../../../r-bridge'
import { sendMessage } from './send'
import { answerForValidationError, validateBaseMessageFormat, validateMessage } from './validate'
import { FileAnalysisRequestMessage, requestAnalysisMessage } from './messages/analysis'
import { SliceRequestMessage } from './messages/slice'
import { requestSliceMessage, SliceRequestMessage } from './messages/slice'
import { FlowrErrorMessage } from './messages/error'

export interface FlowRFileInformation {
Expand Down Expand Up @@ -47,6 +47,7 @@ export class FlowRServerConnection {
break
default:
sendMessage<FlowrErrorMessage>(this.socket, {
id: request.message.id,
type: 'error',
fatal: true,
reason: `The message type ${JSON.stringify(request.type ?? 'undefined')} is not supported.`
Expand All @@ -55,20 +56,16 @@ export class FlowRServerConnection {
}
}

// TODO: do not crash with errors!

// TODO: add name to clients?
// TODO: integrate this with lsp?
private handleFileAnalysisRequest(base: FileAnalysisRequestMessage) {
const requestResult = validateMessage(base, requestAnalysisMessage)
if(requestResult.type === 'error') {
answerForValidationError(this.socket, requestResult)
answerForValidationError(this.socket, requestResult, base.id)
return
}
const message = requestResult.message
console.log(`[${this.name}] Received file analysis request for ${message.filename} (token: ${message.filetoken})`)

// TODO: guard with json schema so that all are correctly keys given
if(this.fileMap.has(message.filetoken)) {
console.log(`File token ${message.filetoken} already exists. Overwriting.`)
}
Expand All @@ -92,34 +89,41 @@ export class FlowRServerConnection {

void slicer.allRemainingSteps(false).then(results => {
sendMessage(this.socket, {
type: 'response-file-analysis',
success: true,
type: 'response-file-analysis',
id: message.id,
results
})
})
}

private handleSliceRequest(request: SliceRequestMessage) {
private handleSliceRequest(base: SliceRequestMessage) {
const requestResult = validateMessage(base, requestSliceMessage)
if(requestResult.type === 'error') {
answerForValidationError(this.socket, requestResult, base.id)
return
}

const request = requestResult.message


console.log(`[${request.filetoken}] Received slice request with criteria ${JSON.stringify(request.criterion)}`)

const fileInformation = this.fileMap.get(request.filetoken)
if(!fileInformation) {
sendMessage<FlowrErrorMessage>(this.socket, {
id: request.id,
type: 'error',
fatal: false,
reason: `The file token ${request.filetoken} has never been analyzed.`
})
return
}
// TODO: remove failed messages as they are part of error?
// TODO: ensure correct criteria
// TODO: cache slices?
// TODO: unique message ids in requests and answsers to link them?
fileInformation.slicer.updateCriterion(request.criterion)
void fileInformation.slicer.allRemainingSteps(true).then(results => {
sendMessage(this.socket, {
type: 'response-slice',
success: true,
id: request.id,
// TODO: is there a better way?
results: Object.fromEntries(Object.entries(results).filter(([k,]) => Object.hasOwn(STEPS_PER_SLICE, k)))
})
Expand Down
1 change: 1 addition & 0 deletions src/cli/repl/server/messages/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const requestAnalysisMessage: RequestMessageDefinition<FileAnalysisReques
type: 'request-file-analysis',
schema: Joi.object({
type: Joi.string().valid('request-file-analysis').required(),
id: Joi.string().optional(),
filetoken: Joi.string().required(),
filename: Joi.string().required(),
content: Joi.string().required()
Expand Down
1 change: 1 addition & 0 deletions src/cli/repl/server/messages/hello.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FlowrBaseMessage } from './messages'

export interface FlowrHelloResponseMessage extends FlowrBaseMessage{
type: 'hello',
id: undefined,
/** a unique name assigned to each client it has no semantic meaning and is only used for debugging */
clientName: string,
versions: VersionInformation
Expand Down
15 changes: 14 additions & 1 deletion src/cli/repl/server/messages/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,25 @@
* @module
*/
import Joi from 'joi'
import { SliceRequestMessage } from './slice'

export interface FlowrBaseMessage {
type: string
/**
* The id that links a request with its responses, it is up to the calling client to make sure it is unique.
* However, the client does not have to pass the id if it does not need to link the request with its response.
* The id is always undefined if the message is unprompted (e.g., with hello) or the id unknown.
*/
id: string | undefined
}

export const baseSchema = Joi.object({ type: Joi.string().required() }).unknown(true)
export const baseMessage: RequestMessageDefinition<FlowrBaseMessage> = {
type: '**base**',
schema: Joi.object({
type: Joi.string().required(),
id: Joi.string().optional()
}).unknown(true)
}

export interface RequestMessageDefinition<T extends FlowrBaseMessage> {
type: T['type']
Expand Down
13 changes: 12 additions & 1 deletion src/cli/repl/server/messages/slice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SlicingCriteria } from '../../../../slicing'
import { LAST_PER_FILE_STEP, LAST_STEP, StepResults } from '../../../../core'
import { FlowrBaseMessage } from './messages'
import { FlowrBaseMessage, RequestMessageDefinition } from './messages'
import Joi from 'joi'

export interface SliceRequestMessage extends FlowrBaseMessage {
type: 'request-slice',
Expand All @@ -13,3 +14,13 @@ export interface SliceResponseMessage extends FlowrBaseMessage {
/** only contains the results of the slice steps to not repeat ourselves */
results: Omit<StepResults<typeof LAST_STEP>, keyof StepResults<typeof LAST_PER_FILE_STEP>>
}

export const requestSliceMessage: RequestMessageDefinition<SliceRequestMessage> = {
type: 'request-slice',
schema: Joi.object({
type: Joi.string().valid('request-slice').required(),
id: Joi.string().optional(),
filetoken: Joi.string().required(),
criterion: Joi.array().items(Joi.string().regex(/\d+:\d+|\d+@.*|\$\d+/)).min(0).required()
})
}
3 changes: 2 additions & 1 deletion src/cli/repl/server/send.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import net from 'node:net'
import { jsonReplacer } from '../../../util/json'
import { FlowrBaseMessage } from './messages/messages'

export function getUnnamedSocketName(c: net.Socket): string {
return `${c.remoteAddress ?? '?'}@${c.remotePort ?? '?'}`
}

export function sendMessage<T>(c: net.Socket, message: T): void {
export function sendMessage<T extends FlowrBaseMessage>(c: net.Socket, message: T): void {
const msg = JSON.stringify(message, jsonReplacer)
console.log(`[${getUnnamedSocketName(c)}] sending message: ${msg}`)
c.write(`${msg}\n`)
Expand Down
6 changes: 4 additions & 2 deletions src/cli/repl/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { FlowrHelloResponseMessage } from './messages/hello'
import { FlowrErrorMessage } from './messages/error'

// TODO: allow to mock server
function notYetInitialized(c: net.Socket) {
function notYetInitialized(c: net.Socket, id: string | undefined) {
sendMessage<FlowrErrorMessage>(c, {
id,
type: 'error',
fatal: true,
reason: 'Server not initialized yet (or failed to), please try again later.'
Expand All @@ -18,6 +19,7 @@ function notYetInitialized(c: net.Socket) {

function helloClient(c: net.Socket, name: string, versionInformation: VersionInformation) {
sendMessage<FlowrHelloResponseMessage>(c, {
id: undefined,
type: 'hello',
clientName: name,
versions: versionInformation
Expand Down Expand Up @@ -50,7 +52,7 @@ export class FlowRServer {

private onConnect(c: net.Socket) {
if(!this.versionInformation) {
notYetInitialized(c)
notYetInitialized(c, undefined)
return
}
// TODO: produce better unique names? :D
Expand Down
22 changes: 15 additions & 7 deletions src/cli/repl/server/validate.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import Joi from 'joi'
import net from 'node:net'
import { sendMessage } from './send'
import { baseSchema, FlowrBaseMessage, RequestMessageDefinition } from './messages/messages'
import { baseMessage, FlowrBaseMessage, RequestMessageDefinition } from './messages/messages'
import { FlowrErrorMessage } from './messages/error'

export interface ValidationErrorResult { type: 'error', reason: Joi.ValidationError }
export interface ValidationErrorResult { type: 'error', reason: Joi.ValidationError | Error }
export interface SuccessValidationResult<T extends FlowrBaseMessage> { type: 'success', message: T }
export type ValidationResult<T extends FlowrBaseMessage> = SuccessValidationResult<T> | ValidationErrorResult

export function validateBaseMessageFormat(input: string): ValidationResult<FlowrBaseMessage> {
const result = baseSchema.validate(JSON.parse(input))
return result.error ? { type: 'error', reason: result.error } : { type: 'success', message: result.value as FlowrBaseMessage }
try {
return validateMessage(JSON.parse(input) as FlowrBaseMessage, baseMessage)
} catch(e) {
return { type: 'error', reason: e as Error }
}
}

export function validateMessage<T extends FlowrBaseMessage>(input: FlowrBaseMessage, def: RequestMessageDefinition<T>): ValidationResult<T> {
const result = def.schema.validate(input)
return result.error ? { type: 'error', reason: result.error } : { type: 'success', message: input as T }
try {
const result = def.schema.validate(input)
return result.error ? { type: 'error', reason: result.error } : { type: 'success', message: input as T }
} catch(e) {
return { type: 'error', reason: e as Error }
}
}

export function answerForValidationError(client: net.Socket, result: ValidationErrorResult): void {
export function answerForValidationError(client: net.Socket, result: ValidationErrorResult, id?: string): void {
sendMessage<FlowrErrorMessage>(client, {
type: 'error',
fatal: false,
id: id,
// TODO: add more information?
reason: `Invalid message format: ${result.reason.message}`
})
Expand Down

0 comments on commit 5b03a36

Please sign in to comment.