init
This commit is contained in:
11
src/access/access.schemas.ts
Normal file
11
src/access/access.schemas.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import z from 'zod';
|
||||
|
||||
const statementSchema = z.object({
|
||||
effect: z.enum(['allow', 'disallow']),
|
||||
resources: z.array(z.string()),
|
||||
actions: z.array(z.string()),
|
||||
});
|
||||
type Statement = z.infer<typeof statementSchema>;
|
||||
|
||||
export type { Statement };
|
||||
export { statementSchema };
|
||||
29
src/access/access.session.ts
Normal file
29
src/access/access.session.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Statement } from './access.schemas.ts';
|
||||
import { validate } from './access.utils.ts';
|
||||
|
||||
type SessionOptions = {
|
||||
statements: Statement[];
|
||||
};
|
||||
|
||||
type ValidateOptions = {
|
||||
action: string;
|
||||
resource: string;
|
||||
};
|
||||
|
||||
class Session {
|
||||
#options: SessionOptions;
|
||||
|
||||
constructor(options: SessionOptions) {
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
public validate = (options: ValidateOptions) => {
|
||||
const { statements } = this.#options;
|
||||
return validate({
|
||||
...options,
|
||||
statements,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { Session };
|
||||
36
src/access/access.token.ts
Normal file
36
src/access/access.token.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { statementSchema } from './access.schemas.ts';
|
||||
|
||||
type AccessTokensOptions = {
|
||||
secret: string | Buffer;
|
||||
};
|
||||
|
||||
const tokenBodySchema = z.object({
|
||||
statements: z.array(statementSchema),
|
||||
});
|
||||
|
||||
type TokenBody = z.infer<typeof tokenBodySchema>;
|
||||
|
||||
class AccessTokens {
|
||||
#options: AccessTokensOptions;
|
||||
|
||||
constructor(options: AccessTokensOptions) {
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
public generate = (options: TokenBody) => {
|
||||
const { secret } = this.#options;
|
||||
const token = jwt.sign(options, secret);
|
||||
return token;
|
||||
};
|
||||
|
||||
public validate = (token: string) => {
|
||||
const { secret } = this.#options;
|
||||
const data = jwt.verify(token, secret);
|
||||
const parsed = tokenBodySchema.parse(data);
|
||||
return parsed;
|
||||
};
|
||||
}
|
||||
|
||||
export { AccessTokens };
|
||||
2
src/access/access.ts
Normal file
2
src/access/access.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './access.session.ts';
|
||||
export * from './access.token.ts';
|
||||
24
src/access/access.utils.ts
Normal file
24
src/access/access.utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import micromatch from 'micromatch';
|
||||
import type { Statement } from './access.schemas.ts';
|
||||
|
||||
type ValidateOptions = {
|
||||
action: string;
|
||||
resource: string;
|
||||
statements: Statement[];
|
||||
};
|
||||
|
||||
const validate = (options: ValidateOptions) => {
|
||||
const { statements, resource, action } = options;
|
||||
const matches = statements.filter(
|
||||
(statement) => micromatch.isMatch(resource, statement.resources) && micromatch.isMatch(action, statement.actions),
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (matches.find((statement) => statement.effect === 'disallow')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { validate };
|
||||
0
src/exports.ts
Normal file
0
src/exports.ts
Normal file
117
src/server/server.ts
Normal file
117
src/server/server.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import http from 'node:http';
|
||||
import tcp from 'node:net';
|
||||
import { WebSocketServer, createWebSocketStream } from 'ws';
|
||||
import {
|
||||
createBroker,
|
||||
type AuthenticateHandler,
|
||||
type AuthorizeForwardHandler,
|
||||
type AuthorizePublishHandler,
|
||||
type AuthorizeSubscribeHandler,
|
||||
type PublishedHandler,
|
||||
} from 'aedes';
|
||||
import { AedesMemoryPersistence } from 'aedes-persistence';
|
||||
import { Session } from '../access/access.session.ts';
|
||||
import type { AccessTokens } from '#root/access/access.token.ts';
|
||||
|
||||
type Aedes = ReturnType<typeof createBroker>;
|
||||
|
||||
declare module 'aedes' {
|
||||
export interface Client {
|
||||
session: Session;
|
||||
}
|
||||
}
|
||||
|
||||
type MqttServerOptions = {
|
||||
accessTokens: AccessTokens;
|
||||
};
|
||||
|
||||
class MqttServer {
|
||||
#options: MqttServerOptions;
|
||||
#server: Aedes;
|
||||
#http?: http.Server;
|
||||
#tcp?: tcp.Server;
|
||||
|
||||
constructor(options: MqttServerOptions) {
|
||||
this.#options = options;
|
||||
this.#server = createBroker({
|
||||
persistence: new AedesMemoryPersistence(),
|
||||
authenticate: this.#authenticate,
|
||||
authorizePublish: this.#authorizePublish,
|
||||
authorizeSubscribe: this.#authorizeSubscribe,
|
||||
authorizeForward: this.#authorizeForward,
|
||||
published: this.#published,
|
||||
});
|
||||
}
|
||||
|
||||
#authenticate: AuthenticateHandler = (client, _username, password, callback) => {
|
||||
if (!password) {
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
const { accessTokens } = this.#options;
|
||||
const auth = accessTokens.validate(password.toString('utf8'));
|
||||
client.session = new Session({
|
||||
statements: auth.statements,
|
||||
});
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
#authorizePublish: AuthorizePublishHandler = (client, packet, callback) => {
|
||||
const authorized = client?.session.validate({
|
||||
action: 'mqtt:publish',
|
||||
resource: `mqtt:${packet.topic}`,
|
||||
});
|
||||
if (!authorized) {
|
||||
return callback(new Error('unauthorized'));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
#authorizeSubscribe: AuthorizeSubscribeHandler = (client, subscription, callback) => {
|
||||
const authorized = client?.session.validate({
|
||||
action: 'mqtt:subscribe',
|
||||
resource: `mqtt:${subscription.topic}`,
|
||||
});
|
||||
if (!authorized) {
|
||||
return callback(new Error('unauthorized'), null);
|
||||
}
|
||||
callback(null, subscription);
|
||||
};
|
||||
|
||||
#authorizeForward: AuthorizeForwardHandler = (client, packet) => {
|
||||
const authorized = client.session.validate({
|
||||
action: 'mqtt:forward',
|
||||
resource: `mqtt:${packet.topic}`,
|
||||
});
|
||||
if (!authorized) {
|
||||
return;
|
||||
}
|
||||
return packet;
|
||||
};
|
||||
|
||||
#published: PublishedHandler = (_packet, _client, callback) => {
|
||||
callback();
|
||||
};
|
||||
|
||||
public getHttpServer = () => {
|
||||
if (!this.#http) {
|
||||
this.#http = http.createServer();
|
||||
const wss = new WebSocketServer({
|
||||
server: this.#http,
|
||||
});
|
||||
wss.on('connection', (websocket, req) => {
|
||||
const stream = createWebSocketStream(websocket);
|
||||
this.#server.handle(stream, req);
|
||||
});
|
||||
}
|
||||
return this.#http;
|
||||
};
|
||||
|
||||
public getTcpServer = () => {
|
||||
if (!this.#tcp) {
|
||||
this.#tcp = tcp.createServer(this.#server.handle);
|
||||
}
|
||||
return this.#tcp;
|
||||
};
|
||||
}
|
||||
|
||||
export { MqttServer };
|
||||
Reference in New Issue
Block a user