Skip to content

Commit

Permalink
Add client-side reverse proxy feature
Browse files Browse the repository at this point in the history
When the UI is accessed over reverse-proxy the url looks like
`https://reverse-proxy-url:443/scdf/` with the actual backend
url like `http://somedomain.de:8080/scdf/`.

If you access `_links` the backend is rendering them with the
backend url rather than the reverse proxy url.

This commit exchanges the protocol / host / port for the
rendered links. For example, `http://somedomain.de:8080/scdf/`
is replaced with `https://reverse-proxy-url:443/scdf/`.

User's have to opt-in to this behavior (checkbox in settings page).
If the server does not run behind a reverse proxy then nothing
is changed.

Resolves #1994
  • Loading branch information
klopfdreh authored and onobc committed Sep 12, 2024
1 parent e53eb89 commit f0f0820
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 23 deletions.
15 changes: 13 additions & 2 deletions ui/src/app/settings/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {SettingModel} from '../shared/model/setting.model';
import {LocalStorageService} from 'angular-2-local-storage';

const DEFAULT_LANG = 'en';
const DEFAULT_REVERSE_PROXY_FIX_ACTIVE = 'false';

@Injectable({
providedIn: 'root'
Expand All @@ -20,9 +21,11 @@ export class SettingsService {
load(languages: Array<string>): Observable<SettingModel[]> {
this.language = languages;
const isDarkConfig = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let activeThemeValue: string = isDarkConfig ? 'dark' : 'default';

let activeThemeValue: string = isDarkConfig ? 'dark' : 'default';
let activeLanguageValue: string = DEFAULT_LANG;
let reverseProxyFixActiveValue: string = DEFAULT_REVERSE_PROXY_FIX_ACTIVE;

if (navigator?.language) {
activeLanguageValue = navigator.language.split('-')[0];
}
Expand All @@ -40,10 +43,15 @@ export class SettingsService {
}
}

if (this.localStorageService.get('reverseProxyFixActiveValue')) {
reverseProxyFixActiveValue = this.localStorageService.get('reverseProxyFixActiveValue');
}

const settings: SettingModel[] = [
{name: 'language-active', value: activeLanguageValue},
{name: 'theme-active', value: activeThemeValue},
{name: 'results-per-page', value: '20'}
{name: 'results-per-page', value: '20'},
{name: 'reverse-proxy-fix-active', value: reverseProxyFixActiveValue}
];
return of(settings).pipe(tap(sett => this.store.dispatch(loaded({settings: sett}))));
}
Expand All @@ -55,6 +63,9 @@ export class SettingsService {
if (setting.name === 'language-active') {
this.localStorageService.set('languageActiveValue', setting.value);
}
if (setting.name === 'reverse-proxy-fix-active') {
this.localStorageService.set('reverseProxyFixActiveValue', setting.value);
}
return from(new Promise<void>(resolve => resolve()));
}

Expand Down
29 changes: 23 additions & 6 deletions ui/src/app/settings/settings/settings.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
<div class="modal-body">
<clr-select-container *ngIf="languageActive && languages?.length > 1">
<label class="clr-col-md-3">{{ 'settings.language' | translate }}</label>
<label class="clr-col-md-4">{{ 'settings.language' | translate }}</label>
<select
clrSelect
name="language"
class="clr-col-md-9"
class="clr-col-md-8"
[value]="languageActive.value"
(change)="languageActiveSettingOnChange($event.target.value)"
>
Expand All @@ -18,11 +18,11 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
<clr-control-helper>{{ 'settings.languageDescription' | translate }}</clr-control-helper>
</clr-select-container>
<clr-select-container *ngIf="themeActive">
<label class="clr-col-md-3">{{ 'settings.theme' | translate }}</label>
<label class="clr-col-md-4">{{ 'settings.theme' | translate }}</label>
<select
clrSelect
name="theme"
class="clr-col-md-9"
class="clr-col-md-8"
[value]="themeActive.value"
(change)="themeActiveSettingOnChange($event.target.value)"
>
Expand All @@ -33,11 +33,11 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
</clr-select-container>

<clr-select-container *ngIf="resultsPerPage">
<label class="clr-col-md-3">{{ 'settings.results' | translate }}</label>
<label class="clr-col-md-4">{{ 'settings.results' | translate }}</label>
<select
clrSelect
name="theme"
class="clr-col-md-9"
class="clr-col-md-8"
[value]="resultsPerPage.value"
(change)="resultPerPageSettingOnChange($event.target.value)"
>
Expand All @@ -50,6 +50,23 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
{{ 'settings.resultsDescription' | translate }}
</clr-control-helper>
</clr-select-container>
<clr-toggle-container>
<label class="clr-col-md-4">{{ 'settings.reverseProxyFix' | translate }}</label>
<clr-toggle-wrapper>
<input
type="checkbox"
clrToggle
name="reverseProxyFix"
class="clr-col-md-8"
[checked]="reverseProxyFix.value === 'true'"
(change)="reverseProxyFixOnChange($event.target.checked + '')"
/>
<label>{{ 'settings.reverseProxyFixActive' | translate }}</label>
</clr-toggle-wrapper>
<clr-control-helper>
{{ 'settings.reverseProxyFixDescription' | translate }}
</clr-control-helper>
</clr-toggle-container>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="isOpen = false" translate>settings.close</button>
Expand Down
5 changes: 4 additions & 1 deletion ui/src/app/settings/settings/settings.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ describe('SettingsComponent', () => {

const initialState = {
[fromSettings.settingsFeatureKey]: {
settings: [{name: fromSettings.themeActiveKey, value: 'default'}]
settings: [
{name: fromSettings.themeActiveKey, value: 'default'},
{name: fromSettings.reverseProxyFixKey, value: 'false'}
]
}
};

Expand Down
6 changes: 6 additions & 0 deletions ui/src/app/settings/settings/settings.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class SettingsComponent extends ModalDialog implements OnInit {
themeActive: SettingModel;
resultsPerPage: SettingModel;
languageActive: SettingModel;
reverseProxyFix: SettingModel;
languages: Array<string>;

constructor(private settingsService: SettingsService) {
Expand All @@ -23,6 +24,7 @@ export class SettingsComponent extends ModalDialog implements OnInit {
this.themeActive = settings.find(st => st.name === 'theme-active');
this.resultsPerPage = settings.find(st => st.name === 'results-per-page');
this.languageActive = settings.find(st => st.name === 'language-active');
this.reverseProxyFix = settings.find(st => st.name === 'reverse-proxy-fix-active');
});
}

Expand All @@ -37,4 +39,8 @@ export class SettingsComponent extends ModalDialog implements OnInit {
languageActiveSettingOnChange(language: string): void {
this.settingsService.dispatch({name: 'language-active', value: language});
}

reverseProxyFixOnChange(active: string): void {
this.settingsService.dispatch({name: 'reverse-proxy-fix-active', value: active});
}
}
9 changes: 6 additions & 3 deletions ui/src/app/settings/store/settings.reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,26 @@ describe('settings/store/settings.reducer.ts', () => {
let expectedState: fromSettings.SettingsState = {
settings: [
{name: fromSettings.themeActiveKey, value: 'value1'},
{name: fromSettings.languageActiveKey, value: 'value1'}
{name: fromSettings.languageActiveKey, value: 'value1'},
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
]
};
let newState = fromSettings.reducer(
undefined,
SettingsActions.loaded({
settings: [
{name: fromSettings.themeActiveKey, value: 'value1'},
{name: fromSettings.languageActiveKey, value: 'value1'}
{name: fromSettings.languageActiveKey, value: 'value1'},
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
]
})
);
expect(newState).toEqual(expectedState);
expectedState = {
settings: [
{name: fromSettings.themeActiveKey, value: 'value2'},
{name: fromSettings.languageActiveKey, value: 'value1'}
{name: fromSettings.languageActiveKey, value: 'value1'},
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
]
};
newState = fromSettings.reducer(
Expand Down
4 changes: 3 additions & 1 deletion ui/src/app/settings/store/settings.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {SettingModel} from '../../shared/model/setting.model';
export const settingsFeatureKey = 'settings';
export const themeActiveKey = 'theme-active';
export const languageActiveKey = 'language-active';
export const reverseProxyFixKey = 'reverse-proxy-fix-active';

export interface SettingsState {
settings: SettingModel[];
Expand All @@ -23,7 +24,8 @@ export const getSetting = (settings: SettingModel[], name: string): string =>
export const initialState: SettingsState = {
settings: [
{name: themeActiveKey, value: 'default'},
{name: languageActiveKey, value: 'en'}
{name: languageActiveKey, value: 'en'},
{name: reverseProxyFixKey, value: 'false'}
]
};

Expand Down
6 changes: 5 additions & 1 deletion ui/src/app/shared/api/schedule.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {TaskService} from './task.service';

describe('shared/api/schedule.service.ts', () => {
let mockHttp;
let mockLocalStorageService;
let scheduleService;
let taskService;
let jsonData = {};
Expand All @@ -15,8 +16,11 @@ describe('shared/api/schedule.service.ts', () => {
post: jasmine.createSpy('post'),
put: jasmine.createSpy('put')
};
mockLocalStorageService = {
get: jasmine.createSpy('get')
};
jsonData = {};
taskService = new TaskService(mockHttp);
taskService = new TaskService(mockHttp, mockLocalStorageService);
scheduleService = new ScheduleService(mockHttp, taskService);
});

Expand Down
7 changes: 6 additions & 1 deletion ui/src/app/shared/api/task.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {TaskExecution} from '../model/task-execution.model';

describe('shared/api/task.service.ts', () => {
let mockHttp;
let mockLocalStorageService;
let taskService;
let jsonData = {};
beforeEach(() => {
Expand All @@ -14,8 +15,11 @@ describe('shared/api/task.service.ts', () => {
post: jasmine.createSpy('post'),
put: jasmine.createSpy('put')
};
mockLocalStorageService = {
get: jasmine.createSpy('get')
};
jsonData = {};
taskService = new TaskService(mockHttp);
taskService = new TaskService(mockHttp, mockLocalStorageService);
});

it('getTasks', () => {
Expand Down Expand Up @@ -144,6 +148,7 @@ describe('shared/api/task.service.ts', () => {

it('getExecutionLogs', () => {
mockHttp.get.and.returnValue(of(jsonData));
mockLocalStorageService.get.and.returnValue(of('false'));
taskService.getExecutionLogs(
TaskExecution.parse({
externalExecutionId: 'foo',
Expand Down
14 changes: 9 additions & 5 deletions ui/src/app/shared/api/task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import {
ValuedConfigurationMetadataPropertyList
} from '../model/detailed-app.model';
import {UrlUtilities} from '../../url-utilities.service';
import {LocalStorageService} from 'angular-2-local-storage';

@Injectable({
providedIn: 'root'
})
export class TaskService {
constructor(protected httpClient: HttpClient) {}
constructor(protected httpClient: HttpClient, private localStorageService: LocalStorageService) {}

getTasks(
page: number,
Expand Down Expand Up @@ -264,10 +265,13 @@ export class TaskService {
`tasks/logs/${taskExecution.externalExecutionId}?platformName=${platformName}&schemaTarget=${taskExecution.schemaTarget}`;
const params = new HttpParams({encoder: new DataflowEncoder()});
return this.httpClient
.get<any>(url, {
headers,
params
})
.get<any>(
UrlUtilities.fixReverseProxyUrl(url, this.localStorageService.get('reverseProxyFixActiveValue') === true),
{
headers,
params
}
)
.pipe(catchError(ErrorUtils.catchError));
}

Expand Down
16 changes: 16 additions & 0 deletions ui/src/app/url-utilities.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,20 @@ export class UrlUtilities {
path += path.endsWith('/') ? '' : '/';
return path;
}

public static fixReverseProxyUrl(url: string, active: boolean) {
if (!active) {
return url;
}
try {
const urlToFix: URL = new URL(url);
const baseUrl: URL = new URL(window.location.href);
urlToFix.host = baseUrl.host;
urlToFix.protocol = baseUrl.protocol;
urlToFix.port = baseUrl.port;
return urlToFix.href;
} catch (_) {
return url;
}
}
}
5 changes: 4 additions & 1 deletion ui/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@
"darkTheme": "Dunkel",
"defaultTheme": "Standard",
"results": "Ergebnisse",
"resultsDescription": "Sie können die Anzahl pro Seite wählen."
"resultsDescription": "Sie können die Anzahl pro Seite wählen.",
"reverseProxyFix": "Reverse-Proxy-Feature",
"reverseProxyFixActive": "Aktivieren",
"reverseProxyFixDescription": "Sie können das clientseitige Reverse-Proxy-Feature aktivieren."
},
"about": {
"user": {
Expand Down
5 changes: 4 additions & 1 deletion ui/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,10 @@
"darkTheme": "dark",
"defaultTheme": "default",
"results": "Results",
"resultsDescription": "You can choose the number of results per page."
"resultsDescription": "You can choose the number of results per page.",
"reverseProxyFix": "Reverse-Proxy-Feature",
"reverseProxyFixActive": "Activate",
"reverseProxyFixDescription": "You can activate the client-side reverse proxy feature."
},
"about": {
"user": {
Expand Down
5 changes: 4 additions & 1 deletion ui/src/assets/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@
"darkTheme": "темная",
"defaultTheme": "По умолчанию",
"results": "Резултат",
"resultsDescription": "Вы можете выбрать количество результатов на странице."
"resultsDescription": "Вы можете выбрать количество результатов на странице.",
"reverseProxyFix": "Функция обратного прокси",
"reverseProxyFixActive": "Активировать",
"reverseProxyFixDescription": "Вы можете активировать функцию обратного прокси-сервера на стороне клиента."
},
"about": {
"user": {
Expand Down

0 comments on commit f0f0820

Please sign in to comment.