Page MenuHomePhabricator

No OneTemporary

diff --git a/keyserver/src/creators/invite-link-creator.js b/keyserver/src/creators/invite-link-creator.js
index f7e52fa46..b0e0d2663 100644
--- a/keyserver/src/creators/invite-link-creator.js
+++ b/keyserver/src/creators/invite-link-creator.js
@@ -1,142 +1,159 @@
// @flow
import Filter from 'bad-words';
import type {
CreateOrUpdatePublicLinkRequest,
InviteLink,
} from 'lib/types/link-types.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
import { ServerError } from 'lib/utils/errors.js';
import { reservedUsernamesSet } from 'lib/utils/reserved-users.js';
import createIDs from './id-creator.js';
import {
dbQuery,
MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE,
SQL,
} from '../database/database.js';
import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js';
import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js';
+import { download, type BlobDownloadResult } from '../services/blob.js';
import { Viewer } from '../session/viewer.js';
const secretRegex = /^[a-zA-Z0-9]+$/;
const badWordsFilter = new Filter();
async function createOrUpdatePublicLink(
viewer: Viewer,
request: CreateOrUpdatePublicLinkRequest,
): Promise<InviteLink> {
if (!secretRegex.test(request.name)) {
throw new ServerError('invalid_characters');
}
if (badWordsFilter.isProfane(request.name)) {
throw new ServerError('offensive_words');
}
if (reservedUsernamesSet.has(request.name)) {
throw new ServerError('link_reserved');
}
const permissionPromise = checkThreadPermission(
viewer,
request.communityID,
threadPermissions.MANAGE_INVITE_LINKS,
);
const existingPrimaryLinksPromise = fetchPrimaryInviteLinks(viewer);
const fetchThreadInfoPromise = fetchServerThreadInfos({
threadID: request.communityID,
});
- const [hasPermission, existingPrimaryLinks, { threadInfos }] =
- await Promise.all([
- permissionPromise,
- existingPrimaryLinksPromise,
- fetchThreadInfoPromise,
- ]);
+ const blobDownloadPromise = getInviteLinkBlob(request);
+ const [
+ hasPermission,
+ existingPrimaryLinks,
+ { threadInfos },
+ blobDownloadResult,
+ ] = await Promise.all([
+ permissionPromise,
+ existingPrimaryLinksPromise,
+ fetchThreadInfoPromise,
+ blobDownloadPromise,
+ ]);
if (!hasPermission) {
throw new ServerError('invalid_credentials');
}
+ if (blobDownloadResult.found) {
+ throw new ServerError('already_in_use');
+ }
const threadInfo = threadInfos[request.communityID];
if (!threadInfo) {
throw new ServerError('invalid_parameters');
}
const defaultRoleID = Object.keys(threadInfo.roles).find(
roleID => threadInfo.roles[roleID].isDefault,
);
if (!defaultRoleID) {
throw new ServerError('invalid_parameters');
}
const existingPrimaryLink = existingPrimaryLinks.find(
link => link.communityID === request.communityID && link.primary,
);
if (existingPrimaryLink) {
const query = SQL`
UPDATE invite_links
SET name = ${request.name}
WHERE \`primary\` = 1 AND community = ${request.communityID}
`;
try {
await dbQuery(query);
} catch (e) {
if (e.errno === MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE) {
throw new ServerError('already_in_use');
}
throw new ServerError('invalid_parameters');
}
return {
name: request.name,
primary: true,
role: defaultRoleID,
communityID: request.communityID,
expirationTime: null,
limitOfUses: null,
numberOfUses: 0,
};
}
const [id] = await createIDs('invite_links', 1);
const row = [id, request.name, true, request.communityID, defaultRoleID];
const createLinkQuery = SQL`
INSERT INTO invite_links(id, name, \`primary\`, community, role)
SELECT ${row}
WHERE NOT EXISTS (
SELECT i.id
FROM invite_links i
WHERE i.\`primary\` = 1 AND i.community = ${request.communityID}
)
`;
let result = null;
const deleteIDs = SQL`
DELETE FROM ids
WHERE id = ${id}
`;
try {
result = (await dbQuery(createLinkQuery))[0];
} catch (e) {
await dbQuery(deleteIDs);
if (e.errno === MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE) {
throw new ServerError('already_in_use');
}
throw new ServerError('invalid_parameters');
}
if (result.affectedRows === 0) {
await dbQuery(deleteIDs);
throw new ServerError('invalid_parameters');
}
return {
name: request.name,
primary: true,
role: defaultRoleID,
communityID: request.communityID,
expirationTime: null,
limitOfUses: null,
numberOfUses: 0,
};
}
+function getInviteLinkBlob(
+ request: CreateOrUpdatePublicLinkRequest,
+): Promise<BlobDownloadResult> {
+ const hash = `invite_${request.name}`;
+ return download(hash);
+}
+
export { createOrUpdatePublicLink };
diff --git a/keyserver/src/services/blob.js b/keyserver/src/services/blob.js
index afd9e5020..69fd97845 100644
--- a/keyserver/src/services/blob.js
+++ b/keyserver/src/services/blob.js
@@ -1,112 +1,112 @@
// @flow
import blobService from 'lib/facts/blob-service.js';
import {
getBlobFetchableURL,
makeBlobServiceEndpointURL,
} from 'lib/utils/blob-service.js';
import { getMessageForException } from 'lib/utils/errors.js';
import { createHTTPAuthorizationHeader } from 'lib/utils/services-utils.js';
import { verifyUserLoggedIn } from '../user/login.js';
import { getContentSigningKey } from '../utils/olm-utils.js';
async function createRequestHeaders(
includeContentType: boolean = true,
): Promise<{ [string]: string }> {
const [{ userId: userID, accessToken }, deviceID] = await Promise.all([
verifyUserLoggedIn(),
getContentSigningKey(),
]);
const authorization = createHTTPAuthorizationHeader({
userID,
deviceID,
accessToken,
});
return {
Authorization: authorization,
...(includeContentType && { 'Content-Type': 'application/json' }),
};
}
async function uploadBlob(blob: Blob, hash: string): Promise<void> {
const formData = new FormData();
formData.append('blob_hash', hash);
formData.append('blob_data', blob);
const headers = await createRequestHeaders(false);
const uploadBlobResponse = await fetch(
makeBlobServiceEndpointURL(blobService.httpEndpoints.UPLOAD_BLOB),
{
method: blobService.httpEndpoints.UPLOAD_BLOB.method,
body: formData,
headers,
},
);
if (!uploadBlobResponse.ok) {
const { status, statusText } = uploadBlobResponse;
throw new Error(`Payload upload failed with HTTP ${status}: ${statusText}`);
}
}
async function assignHolder(holder: string, hash: string): Promise<void> {
const headers = await createRequestHeaders();
const assignHolderResponse = await fetch(
makeBlobServiceEndpointURL(blobService.httpEndpoints.ASSIGN_HOLDER),
{
method: blobService.httpEndpoints.ASSIGN_HOLDER.method,
body: JSON.stringify({
holder,
blob_hash: hash,
}),
headers,
},
);
if (!assignHolderResponse.ok) {
const { status, statusText } = assignHolderResponse;
throw new Error(
`Holder assignment failed with HTTP ${status}: ${statusText}`,
);
}
}
async function upload(blob: Blob, hash: string, holder: string): Promise<void> {
try {
await Promise.all([assignHolder(holder, hash), uploadBlob(blob, hash)]);
} catch (e) {
throw new Error(
`Payload upload failed with: ${
getMessageForException(e) ?? 'unknown error'
}`,
);
}
}
-async function download(hash: string): Promise<
+export type BlobDownloadResult =
| {
+found: false,
}
| {
+found: true,
+blob: Blob,
- },
-> {
+ };
+async function download(hash: string): Promise<BlobDownloadResult> {
const url = getBlobFetchableURL(hash);
const headers = await createRequestHeaders();
const response = await fetch(url, {
method: blobService.httpEndpoints.GET_BLOB.method,
headers,
});
if (!response.ok) {
return { found: false };
}
const blob = await response.blob();
return { found: true, blob };
}
export { upload, uploadBlob, assignHolder, download };

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 1:25 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2559303
Default Alt Text
(8 KB)

Event Timeline