Skip to content

Commit

Permalink
feat(http): create http frontend client
Browse files Browse the repository at this point in the history
- Create HTTP client based on old services
- Create HTTP client request interceptor: request
- Create HTTP client generic: GenericRequest
- Create HTTP client server: WzRequest, ApiCheck and WzAuthentication
- Enhance server API backend client
  See #6995
- Rename ILogger type to Logger
  • Loading branch information
Desvelao committed Sep 19, 2024
1 parent 052d464 commit 81616ca
Show file tree
Hide file tree
Showing 16 changed files with 1,157 additions and 33 deletions.
13 changes: 3 additions & 10 deletions plugins/main/server/controllers/wazuh-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,9 @@ export class WazuhApiCtrl {
}
}
}
let token;
if (context.wazuh_core.manageHosts.isEnabledAuthWithRunAs(idHost)) {
token = await context.wazuh.api.client.asCurrentUser.authenticate(
idHost,
);
} else {
token = await context.wazuh.api.client.asInternalUser.authenticate(
idHost,
);
}
const token = await context.wazuh.api.client.asCurrentUser.authenticate(
idHost,
);

let textSecure = '';
if (context.wazuh.server.info.protocol === 'https') {
Expand Down
4 changes: 2 additions & 2 deletions plugins/wazuh-core/common/services/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash';
import { formatLabelValuePair } from './settings';
import { formatBytes } from './file-size';

export interface ILogger {
export interface Logger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
Expand Down Expand Up @@ -180,7 +180,7 @@ export class Configuration implements IConfiguration {
store: IConfigurationStore | null = null;
_settings: Map<string, { [key: string]: TConfigurationSetting }>;
_categories: Map<string, { [key: string]: any }>;
constructor(private logger: ILogger, store: IConfigurationStore) {
constructor(private logger: Logger, store: IConfigurationStore) {
this._settings = new Map();
this._categories = new Map();
this.setStore(store);
Expand Down
26 changes: 25 additions & 1 deletion plugins/wazuh-core/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '../common/constants';
import { DashboardSecurity } from './utils/dashboard-security';
import * as hooks from './hooks';
import { CoreHTTPClient } from './services/http/http-client';

export class WazuhCorePlugin
implements Plugin<WazuhCorePluginSetup, WazuhCorePluginStart>
Expand All @@ -19,12 +20,21 @@ export class WazuhCorePlugin
services: { [key: string]: any } = {};
public async setup(core: CoreSetup): Promise<WazuhCorePluginSetup> {
const noop = () => {};
const logger = {
// Debug logger
const consoleLogger = {
info: console.log,
error: console.error,
debug: console.debug,
warn: console.warn,
};
// No operation logger
const noopLogger = {
info: noop,
error: noop,
debug: noop,
warn: noop,
};
const logger = noopLogger;
this._internal.configurationStore = new ConfigurationStore(
logger,
core.http,
Expand All @@ -44,9 +54,22 @@ export class WazuhCorePlugin
this.services.configuration.registerCategory({ ...value, id: key });
});

// Create dashboardSecurity
this.services.dashboardSecurity = new DashboardSecurity(logger, core.http);

// Create http
this.services.http = new CoreHTTPClient(logger, {
getTimeout: async () =>
(await this.services.configuration.get('timeout')) as number,
getURL: (path: string) => core.http.basePath.prepend(path),
getServerAPI: () => 'api-host-id', // TODO: implement
getIndexPatternTitle: async () => 'wazuh-alerts-*', // TODO: implement
http: core.http,
});

// Setup services
await this.services.dashboardSecurity.setup();
await this.services.http.setup();

return {
...this.services,
Expand All @@ -60,6 +83,7 @@ export class WazuhCorePlugin
setCore(core);
setUiSettings(core.uiSettings);

// Start services
await this.services.configuration.start({ http: core.http });

return {
Expand Down
105 changes: 105 additions & 0 deletions plugins/wazuh-core/public/services/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# HTTPClient

The `HTTPClient` provides a custom mechanim to do an API request to the backend side.

This defines a request interceptor that disables the requests when `core.http` returns a response with status code 401, avoiding a problem in the login flow (with SAML).

The request interceptor is used in the clients:

- generic
- server

## Generic

This client provides a method to run the request that injects some properties related to an index pattern and selected server API host in the headers of the API request that could be used for some backend endpoints

### Usage

#### Request

```ts
plugins.wazuhCore.http.request('GET', '/api/check-api', {});
```

## Server

This client provides:

- some methods to communicate with the Wazuh server API
- manage authentication with Wazuh server API
- store the login data

### Usage

#### Authentication

```ts
plugins.wazuhCore.http.auth();
```

#### Unauthentication

```ts
plugins.wazuhCore.http.unauth();
```

#### Request

```ts
plugins.wazuhCore.http.request('GET', '/agents', {});
```

#### CSV

```ts
plugins.wazuhCore.http.csv('GET', '/agents', {});
```

#### Check API id

```ts
plugins.wazuhCore.http.checkApiById('api-host-id');
```

#### Check API

```ts
plugins.wazuhCore.http.checkApi(apiHostData);
```

#### Get user data

```ts
plugins.wazuhCore.http.getUserData();
```

The changes in the user data can be retrieved thourgh the `userData$` observable.

```ts
plugins.wazuhCore.http.userData$.subscribe(userData => {
// do something with the data
});
```

### Register interceptor

In each application when this is mounted through the `mount` method, the request interceptor must be registered and when the application is unmounted must be unregistered.

> We should research about the possibility to register/unregister the interceptor once in the `wazuh-core` plugin instead of registering/unregisting in each mount of application.
```ts
// setup lifecycle plugin method

// Register an application
core.application.register({
// rest of registration properties
mount: () => {
// Register the interceptor
plugins.wazuhCore.http.register();
return () => {
// Unregister the interceptor
plugins.wazuhCore.http.unregister();
};
},
});
```
5 changes: 5 additions & 0 deletions plugins/wazuh-core/public/services/http/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const PLUGIN_PLATFORM_REQUEST_HEADERS = {
'osd-xsrf': 'kibana',
};

export const HTTP_CLIENT_DEFAULT_TIMEOUT = 20000;
127 changes: 127 additions & 0 deletions plugins/wazuh-core/public/services/http/generic-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { PLUGIN_PLATFORM_REQUEST_HEADERS } from './constants';
import { Logger } from '../../../common/services/configuration';
import {
HTTPClientGeneric,
HTTPClientRequestInterceptor,
HTTPVerb,
} from './types';

interface GenericRequestServices {
request: HTTPClientRequestInterceptor['request'];
getURL(path: string): string;
getTimeout(): Promise<number>;
getIndexPatternTitle(): Promise<string>;
getServerAPI(): string;
checkAPIById(apiId: string): Promise<any>;
}

export class GenericRequest implements HTTPClientGeneric {
onErrorInterceptor?: (error: any) => Promise<void>;
constructor(
private logger: Logger,
private services: GenericRequestServices,
) {}
async request(
method: HTTPVerb,
path: string,
payload = null,
returnError = false,
) {
try {
if (!method || !path) {
throw new Error('Missing parameters');
}
const timeout = await this.services.getTimeout();
const requestHeaders = {
...PLUGIN_PLATFORM_REQUEST_HEADERS,
'content-type': 'application/json',
};
const url = this.services.getURL(path);

try {
requestHeaders.pattern = await this.services.getIndexPatternTitle();
} catch (error) {}

try {
requestHeaders.id = this.services.getServerAPI();
} catch (error) {
// Intended
}
var options = {};

if (method === 'GET') {
options = {
method: method,
headers: requestHeaders,
url: url,
timeout: timeout,
};
}
if (method === 'PUT') {
options = {
method: method,
headers: requestHeaders,
data: payload,
url: url,
timeout: timeout,
};
}
if (method === 'POST') {
options = {
method: method,
headers: requestHeaders,
data: payload,
url: url,
timeout: timeout,
};
}
if (method === 'DELETE') {
options = {
method: method,
headers: requestHeaders,
data: payload,
url: url,
timeout: timeout,
};
}

const data = await this.services.request(options);
if (!data) {
throw new Error(`Error doing a request to ${url}, method: ${method}.`);
}

return data;
} catch (error) {
//if the requests fails, we need to check if the API is down
const currentApi = this.services.getServerAPI(); //JSON.parse(AppState.getCurrentAPI() || '{}');
if (currentApi) {
try {
await this.services.checkAPIById(currentApi);
} catch (err) {
// const wzMisc = new WzMisc();
// wzMisc.setApiIsDown(true);
// if (
// ['/settings', '/health-check', '/blank-screen'].every(
// pathname =>
// !NavigationService.getInstance()
// .getPathname()
// .startsWith(pathname),
// )
// ) {
// NavigationService.getInstance().navigate('/health-check');
// }
}
}
// if(this.onErrorInterceptor){
// await this.onErrorInterceptor(error)
// }
if (returnError) return Promise.reject(error);
return (((error || {}).response || {}).data || {}).message || false
? Promise.reject(new Error(error.response.data.message))
: Promise.reject(error || new Error('Server did not respond'));
}
}
setOnErrorInterceptor(onErrorInterceptor: (error: any) => Promise<void>) {
this.onErrorInterceptor = onErrorInterceptor;
}
}
Loading

0 comments on commit 81616ca

Please sign in to comment.