2020-03-23 15:35:58 +01:00
|
|
|
/*
|
|
|
|
|
* MIT License
|
|
|
|
|
*
|
|
|
|
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
|
|
|
* copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
* SOFTWARE.
|
|
|
|
|
*/
|
|
|
|
|
|
2019-10-20 16:59:02 +02:00
|
|
|
import { contextPath } from "./urls";
|
2019-12-11 10:11:40 +01:00
|
|
|
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors";
|
|
|
|
|
|
|
|
|
|
type SubscriptionEvent = {
|
|
|
|
|
type: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type OpenEvent = SubscriptionEvent;
|
|
|
|
|
|
|
|
|
|
type ErrorEvent = SubscriptionEvent & {
|
|
|
|
|
error: Error;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MessageEvent = SubscriptionEvent & {
|
|
|
|
|
data: string;
|
|
|
|
|
lastEventId?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MessageListeners = {
|
|
|
|
|
[eventType: string]: (event: MessageEvent) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SubscriptionContext = {
|
|
|
|
|
onOpen?: OpenEvent;
|
|
|
|
|
onMessage: MessageListeners;
|
|
|
|
|
onError?: ErrorEvent;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type SubscriptionArgument = MessageListeners | SubscriptionContext;
|
|
|
|
|
|
|
|
|
|
type Cancel = () => void;
|
2018-07-04 16:43:46 +02:00
|
|
|
|
2019-11-13 14:03:48 +01:00
|
|
|
const sessionId = (
|
|
|
|
|
Date.now().toString(36) +
|
|
|
|
|
Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.substr(2, 5)
|
|
|
|
|
).toUpperCase();
|
|
|
|
|
|
2019-11-18 11:45:48 +01:00
|
|
|
const extractXsrfTokenFromJwt = (jwt: string) => {
|
|
|
|
|
const parts = jwt.split(".");
|
|
|
|
|
if (parts.length === 3) {
|
|
|
|
|
return JSON.parse(atob(parts[1])).xsrf;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// @VisibleForTesting
|
|
|
|
|
export const extractXsrfTokenFromCookie = (cookieString?: string) => {
|
|
|
|
|
if (cookieString) {
|
|
|
|
|
const cookies = cookieString.split(";");
|
|
|
|
|
for (const c of cookies) {
|
|
|
|
|
const parts = c.trim().split("=");
|
|
|
|
|
if (parts[0] === "X-Bearer-Token") {
|
|
|
|
|
return extractXsrfTokenFromJwt(parts[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const extractXsrfToken = () => {
|
|
|
|
|
return extractXsrfTokenFromCookie(document.cookie);
|
|
|
|
|
};
|
|
|
|
|
|
2019-12-11 10:11:40 +01:00
|
|
|
const createRequestHeaders = () => {
|
|
|
|
|
const headers: { [key: string]: string } = {
|
|
|
|
|
// disable caching for now
|
|
|
|
|
Cache: "no-cache",
|
|
|
|
|
// identify the request as ajax request
|
|
|
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
|
|
|
// identify the web interface
|
|
|
|
|
"X-SCM-Client": "WUI",
|
|
|
|
|
// identify the window session
|
|
|
|
|
"X-SCM-Session-ID": sessionId
|
|
|
|
|
};
|
2019-11-18 11:45:48 +01:00
|
|
|
|
|
|
|
|
const xsrf = extractXsrfToken();
|
|
|
|
|
if (xsrf) {
|
|
|
|
|
headers["X-XSRF-Token"] = xsrf;
|
|
|
|
|
}
|
2019-12-11 10:11:40 +01:00
|
|
|
return headers;
|
|
|
|
|
};
|
2019-11-18 11:45:48 +01:00
|
|
|
|
2019-12-11 10:11:40 +01:00
|
|
|
const applyFetchOptions: (p: RequestInit) => RequestInit = o => {
|
|
|
|
|
if (o.headers) {
|
|
|
|
|
o.headers = {
|
|
|
|
|
...createRequestHeaders()
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
o.headers = createRequestHeaders();
|
|
|
|
|
}
|
2019-10-20 16:59:02 +02:00
|
|
|
o.credentials = "same-origin";
|
2019-09-04 14:47:13 +02:00
|
|
|
return o;
|
2018-07-04 16:43:46 +02:00
|
|
|
};
|
|
|
|
|
|
2018-11-15 21:39:08 +01:00
|
|
|
function handleFailure(response: Response) {
|
2018-07-04 16:43:46 +02:00
|
|
|
if (!response.ok) {
|
2018-11-15 21:39:08 +01:00
|
|
|
if (isBackendError(response)) {
|
2019-10-19 16:38:07 +02:00
|
|
|
return response.json().then((content: BackendErrorContent) => {
|
2020-08-20 17:44:36 +02:00
|
|
|
if (content.errorCode === "DDS8D8unr1") {
|
|
|
|
|
window.location.replace(`${contextPath}/login`);
|
|
|
|
|
throw new UnauthorizedError("Unauthorized", 401);
|
|
|
|
|
} else {
|
|
|
|
|
throw createBackendError(content, response.status);
|
|
|
|
|
}
|
2019-10-19 16:38:07 +02:00
|
|
|
});
|
2018-11-15 21:39:08 +01:00
|
|
|
} else {
|
2019-02-25 17:40:53 +01:00
|
|
|
if (response.status === 401) {
|
2019-10-20 16:59:02 +02:00
|
|
|
throw new UnauthorizedError("Unauthorized", 401);
|
2019-03-04 11:49:12 +01:00
|
|
|
} else if (response.status === 403) {
|
2019-10-20 16:59:02 +02:00
|
|
|
throw new ForbiddenError("Forbidden", 403);
|
2019-02-25 17:40:53 +01:00
|
|
|
}
|
2019-03-04 11:49:12 +01:00
|
|
|
|
2019-10-20 16:59:02 +02:00
|
|
|
throw new Error("server returned status code " + response.status);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return response;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-12 08:22:27 +02:00
|
|
|
export function createUrl(url: string) {
|
2019-10-20 16:59:02 +02:00
|
|
|
if (url.includes("://")) {
|
2018-07-11 12:02:53 +02:00
|
|
|
return url;
|
|
|
|
|
}
|
2018-07-12 08:22:27 +02:00
|
|
|
let urlWithStartingSlash = url;
|
2019-10-20 16:59:02 +02:00
|
|
|
if (url.indexOf("/") !== 0) {
|
|
|
|
|
urlWithStartingSlash = "/" + urlWithStartingSlash;
|
2018-07-11 22:01:36 +02:00
|
|
|
}
|
2018-10-01 17:22:03 +02:00
|
|
|
return `${contextPath}/api/v2${urlWithStartingSlash}`;
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:15:35 +01:00
|
|
|
export function createUrlWithIdentifiers(url: string): string {
|
|
|
|
|
return createUrl(url) + "?X-SCM-Client=WUI&X-SCM-Session-ID=" + sessionId;
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-04 16:43:46 +02:00
|
|
|
class ApiClient {
|
2018-07-11 12:02:53 +02:00
|
|
|
get(url: string): Promise<Response> {
|
2019-09-26 15:42:21 +02:00
|
|
|
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
post(url: string, payload?: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
2019-11-20 08:29:05 +01:00
|
|
|
return this.httpRequestWithJSONBody("POST", url, contentType, additionalHeaders, payload);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
postText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
|
2019-11-20 08:59:57 +01:00
|
|
|
return this.httpRequestWithTextBody("POST", url, additionalHeaders, payload);
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
putText(url: string, payload: string, additionalHeaders: Record<string, string> = {}) {
|
2019-11-20 08:59:57 +01:00
|
|
|
return this.httpRequestWithTextBody("PUT", url, additionalHeaders, payload);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
postBinary(url: string, fileAppender: (p: FormData) => void, additionalHeaders: Record<string, string> = {}) {
|
2019-10-21 10:57:56 +02:00
|
|
|
const formData = new FormData();
|
2019-08-30 09:11:14 +02:00
|
|
|
fileAppender(formData);
|
2019-08-29 16:37:57 +02:00
|
|
|
|
2019-10-21 10:57:56 +02:00
|
|
|
const options: RequestInit = {
|
2019-10-20 16:59:02 +02:00
|
|
|
method: "POST",
|
2019-11-20 08:29:05 +01:00
|
|
|
body: formData,
|
|
|
|
|
headers: additionalHeaders
|
2019-08-29 16:37:57 +02:00
|
|
|
};
|
2019-08-30 09:11:14 +02:00
|
|
|
return this.httpRequestWithBinaryBody(options, url);
|
2019-08-29 16:37:57 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
put(url: string, payload: any, contentType = "application/json", additionalHeaders: Record<string, string> = {}) {
|
2019-11-20 08:29:05 +01:00
|
|
|
return this.httpRequestWithJSONBody("PUT", url, contentType, additionalHeaders, payload);
|
2018-07-11 17:02:38 +02:00
|
|
|
}
|
|
|
|
|
|
2018-10-25 13:45:52 +02:00
|
|
|
head(url: string) {
|
2019-10-20 16:59:02 +02:00
|
|
|
let options: RequestInit = {
|
|
|
|
|
method: "HEAD"
|
2018-10-25 13:45:52 +02:00
|
|
|
};
|
2019-09-04 14:47:13 +02:00
|
|
|
options = applyFetchOptions(options);
|
2018-11-15 21:39:08 +01:00
|
|
|
return fetch(createUrl(url), options).then(handleFailure);
|
2018-10-15 16:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
2018-07-11 12:02:53 +02:00
|
|
|
delete(url: string): Promise<Response> {
|
2019-10-20 16:59:02 +02:00
|
|
|
let options: RequestInit = {
|
|
|
|
|
method: "DELETE"
|
2018-07-04 16:43:46 +02:00
|
|
|
};
|
2019-09-04 14:47:13 +02:00
|
|
|
options = applyFetchOptions(options);
|
2018-11-15 21:39:08 +01:00
|
|
|
return fetch(createUrl(url), options).then(handleFailure);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 08:29:05 +01:00
|
|
|
httpRequestWithJSONBody(
|
|
|
|
|
method: string,
|
|
|
|
|
url: string,
|
|
|
|
|
contentType: string,
|
2019-11-20 10:44:11 +01:00
|
|
|
additionalHeaders: Record<string, string>,
|
2019-11-20 08:29:05 +01:00
|
|
|
payload?: any
|
|
|
|
|
): Promise<Response> {
|
2019-10-21 10:57:56 +02:00
|
|
|
const options: RequestInit = {
|
2018-07-04 16:43:46 +02:00
|
|
|
method: method,
|
2019-11-20 08:29:05 +01:00
|
|
|
headers: additionalHeaders
|
2018-07-04 16:43:46 +02:00
|
|
|
};
|
2019-11-05 16:10:41 +01:00
|
|
|
if (payload) {
|
|
|
|
|
options.body = JSON.stringify(payload);
|
|
|
|
|
}
|
2019-08-30 09:11:14 +02:00
|
|
|
return this.httpRequestWithBinaryBody(options, url, contentType);
|
2019-08-29 16:37:57 +02:00
|
|
|
}
|
|
|
|
|
|
2019-11-20 10:44:11 +01:00
|
|
|
httpRequestWithTextBody(
|
|
|
|
|
method: string,
|
|
|
|
|
url: string,
|
|
|
|
|
additionalHeaders: Record<string, string> = {},
|
|
|
|
|
payload: string
|
|
|
|
|
) {
|
2019-11-20 08:59:57 +01:00
|
|
|
const options: RequestInit = {
|
|
|
|
|
method: method,
|
|
|
|
|
headers: additionalHeaders
|
|
|
|
|
};
|
|
|
|
|
options.body = payload;
|
|
|
|
|
return this.httpRequestWithBinaryBody(options, url, "text/plain");
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-21 10:57:56 +02:00
|
|
|
httpRequestWithBinaryBody(options: RequestInit, url: string, contentType?: string) {
|
2019-09-04 14:47:13 +02:00
|
|
|
options = applyFetchOptions(options);
|
2019-08-30 09:11:14 +02:00
|
|
|
if (contentType) {
|
2019-10-20 16:59:02 +02:00
|
|
|
if (!options.headers) {
|
2019-11-20 11:12:22 +01:00
|
|
|
options.headers = {};
|
2019-10-20 16:59:02 +02:00
|
|
|
}
|
2019-12-11 10:11:40 +01:00
|
|
|
// @ts-ignore We are sure that here we only get headers of type {[name:string]: string}
|
2019-10-20 16:59:02 +02:00
|
|
|
options.headers["Content-Type"] = contentType;
|
2019-08-30 09:11:14 +02:00
|
|
|
}
|
2018-07-04 16:43:46 +02:00
|
|
|
|
2018-11-15 21:39:08 +01:00
|
|
|
return fetch(createUrl(url), options).then(handleFailure);
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
2019-12-11 10:11:40 +01:00
|
|
|
|
|
|
|
|
subscribe(url: string, argument: SubscriptionArgument): Cancel {
|
2020-03-20 11:15:35 +01:00
|
|
|
const es = new EventSource(createUrlWithIdentifiers(url), {
|
|
|
|
|
withCredentials: true
|
2019-12-11 10:11:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let listeners: MessageListeners;
|
|
|
|
|
// type guard, to identify that argument is of type SubscriptionContext
|
|
|
|
|
if ("onMessage" in argument) {
|
|
|
|
|
listeners = (argument as SubscriptionContext).onMessage;
|
|
|
|
|
if (argument.onError) {
|
2020-03-20 11:15:35 +01:00
|
|
|
// @ts-ignore typing of EventSource is weird
|
2019-12-11 10:11:40 +01:00
|
|
|
es.onerror = argument.onError;
|
|
|
|
|
}
|
|
|
|
|
if (argument.onOpen) {
|
2020-03-20 11:15:35 +01:00
|
|
|
// @ts-ignore typing of EventSource is weird
|
2019-12-11 10:11:40 +01:00
|
|
|
es.onopen = argument.onOpen;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
listeners = argument;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const type in listeners) {
|
2020-03-20 11:15:35 +01:00
|
|
|
// @ts-ignore typing of EventSource is weird
|
2019-12-11 10:11:40 +01:00
|
|
|
es.addEventListener(type, listeners[type]);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-17 09:30:21 +01:00
|
|
|
return () => es.close();
|
2019-12-11 10:11:40 +01:00
|
|
|
}
|
2018-07-04 16:43:46 +02:00
|
|
|
}
|
|
|
|
|
|
2019-10-21 10:57:56 +02:00
|
|
|
export const apiClient = new ApiClient();
|