1
0
mirror of https://gitlab.com/comunic/comunicapiv2 synced 2024-11-26 23:39:22 +00:00
comunicapiv2/src/entities/RequestHandler.ts
2020-03-30 16:14:40 +01:00

626 lines
16 KiB
TypeScript

import { conf } from "../helpers/ConfigHelper";
import { Response, Request } from "express";
import { APIHelper } from "../helpers/APIHelper";
import { APIClient } from "./APIClient";
import { checkMail, removeHTMLNodes, checkURL } from "../utils/StringUtils";
import { AccountHelper } from "../helpers/AccountHelper";
import { UploadedFile } from "express-fileupload";
import { prepareFileCreation, generateNewUserDataFileName, pathUserData } from "../utils/UserDataUtils";
import * as sharp from 'sharp';
import { UserHelper } from "../helpers/UserHelper";
import { GroupsAccessLevel } from "./Group";
import { GroupsHelper } from "../helpers/GroupsHelper";
import { checkVirtualDirectory } from "../utils/VirtualDirsUtils";
import { FriendsHelper } from "../helpers/FriendsHelper";
import { PostsHelper } from "../helpers/PostsHelper";
import { PostAccessLevel } from "./Post";
import { writeFileSync } from "fs";
import { CommentsHelper } from "../helpers/CommentsHelper";
/**
* Response to a request
*
* @author Pierre HUBERT
*/
export class RequestHandler {
private client : APIClient = null;
private userID : number = -1;
private responseSent = false;
public constructor(private req : Request, private response : Response) {}
/**
* Get remote IP address
*/
public get remoteIP() : string {
// TODO : check in production
return this.req.ip;
}
/**
* Get a parameter included in the post request
*
* @param name Name of the parameter to get
*/
private getPostParam(name : string) : any {
return this.req.body[name];
}
/**
* Get a String from the request
*
* @param name The name of the string to get
* @param minLength Minimal required size of the string
* @param required If set to true (true by default), an error will
* be thrown if the string is not included in the request
*/
public postString(name : string, minLength : number = 1, required : boolean = true) : string {
const param = this.getPostParam(name);
// Check if parameter was not found
if(param == undefined) {
if(required)
this.error(400, "Could not find required string: '"+name+"'");
return "";
}
if(param.length < minLength)
this.error(400, "Parameter "+name+" is too short!");
return param;
}
/**
* Check out whether a post parameter is present into the request or not
*
* @param name The name of the post field to check
*/
public hasPostParameter(name: string) : boolean {
return this.getPostParam(name) != undefined;
}
/**
* Check out whether a POST string is present in the request or not
*
* @param name The name of the POST field to check
* @param minLength Minimal length of the parameter
*/
public hasPostString(name: string, minLength: number = 0) : boolean {
return this.hasPostParameter(name) && this.getPostParam(name).length >= minLength;
}
/**
* Get some content for post and satinize it (remove HTML nodes)
*
* @param name The name of the POST field
* @param minLength Optionnal minimal length for the post
*/
public postContent(name: string, minLength ?: number) : string {
const content = this.postString(name, minLength);
if(content.match(/data:image/))
this.error(401, "Please do not include inline images!");
return removeHTMLNodes(content);
}
/**
* Get an email address included in a post request
*
* @param name The name of the POST filed
*/
public postEmail(name: string) : string {
const email = this.postString(name, 3);
if(!checkMail(email))
this.error(400, email + " is not a valid email address!");
return email;
}
/**
* Get an integer included in the request
*
* @param name Name of POST field
* @param fallback Fallback value (if none, throw an error)
* @returns The number (throws in case of error)
*/
public postInt(name: string, fallback ?: number) : number {
const param = this.getPostParam(name);
if(param == undefined) {
if(fallback == undefined)
this.error(400, "Missing integer '"+name+"' in the request!");
return fallback;
}
// Check number
if(Number.parseInt(param).toString() !== param.toString())
this.error(400, "'"+name+"' is an invalid integer!");
return Number.parseInt(param);
}
/**
* Get a list of integeres included in the request
*
* @param name The name of the post field
* @param minEntries Specify the minimum number of entries required
*/
public postNumbersList(name: string, minEntries : number = 1) : Array<number> {
const param = this.postString(name, minEntries < 1 ? 0 : minEntries, minEntries > 0);
let list = [];
for (const el of param.split(",")) {
if(el == "")
continue;
if(Number.parseInt(el).toString() != el)
this.error(400, "Invalid number detected in '"+name+"'!");
list.push(Number.parseInt(el));
}
if(list.length < minEntries)
this.error(400, "Not enough entries in '" + name + "'!")
return list;
}
/**
* Turn a list of string into a Set object
*
* @param name Name of POST field
* @param minEntries Minimum number of entries to specify
*/
public postNumbersSet(name : string, minEntries : number = 1) : Set<number> {
return new Set(this.postNumbersList(name, minEntries));
}
/**
* Attempt to decode JSON included in a POST request
*
* @param name Name of POST field
*/
public postJSON(name: string) : any {
const src = this.getPostParam(name);
if(src == undefined)
this.error(400, "Missing JSON '" + name + "' in the request!");
try {
const response = JSON.parse(src);
return response;
} catch(e) {
this.error(500, "'" + name + "' is not a valid JSON !");
}
}
/**
* Get a boolean included in the request
*
* @param name The name of the POST field
* @param fallback Fallback value to use if the value is not
* found in the request
*/
public postBool(name: string, fallback ?: boolean) : boolean {
const param = this.getPostParam(name);
if(param == undefined) {
if(fallback != undefined)
return fallback;
this.error(400, "Missing boolean '" + name + "' in the request!");
}
return param === "true" || param === true;
}
/**
* Get the ID of a user specified in a POST request
*
* @param name Name of the POST field
*/
public async postUserId(name: string) : Promise<number> {
const userID = this.postInt(name);
if(userID < 1)
this.error(400, "Invalid user ID specified in '" + name +"'!");
if(!await UserHelper.Exists(userID))
this.error(404, "User with ID " + userID + " not found!");
return userID;
}
/**
* Get the ID of a friend included in a POST request
*
* @param name Name of the POST field
*/
public async postFriendId(name: string) : Promise<number> {
const friendID = await this.postUserId(name);
if(!await FriendsHelper.AreFriend(this.getUserId(), friendID))
this.error(401, "You are not friend with this personn!");
return friendID;
}
/**
* Find user ID based on its email address, included in a POST request
*
* @param name The name of the POST field containing the email address of the user
*/
public async postUserIdFromEmail(name: string) : Promise<number> {
const email = this.postEmail(name);
const userID = await AccountHelper.FindIDFromEmail(email);
if(userID < 1)
this.error(404, "Email not found!");
return userID;
}
/**
* Get a POST group ID
*
* @param name The name of the POST field
*/
public async postGroupID(name: string) : Promise<number> {
const groupID = this.postInt(name);
if(!await GroupsHelper.Exists(groupID))
this.error(404, "Specified group not found!");
return groupID;
}
/**
* Get a POST group ID with a check for access level of current user
*
* @param name The name of the POST field containing group ID
* @param minVisibility Minimum visiblity requested to the group
* @returns The ID of the group (throws in case of failure)
*/
public async postGroupIDWithAccess(name: string, minVisibility : GroupsAccessLevel) : Promise<number> {
const groupID = await this.postGroupID(name);
const access = await GroupsHelper.GetAccessLevel(groupID, this.optionnalUserID);
if(access == GroupsAccessLevel.NO_ACCESS)
this.error(404, "Specified group not found!");
if(access < minVisibility)
this.error(401, "You do not have enough rights to perform what you intend to do on this group!");
return groupID;
}
/**
* Get the ID of post included in a POST request
*
* @param name The name of the POST field containing the id of the target post
*/
public async postPostID(name: string) : Promise<number> {
const postID = this.postInt(name);
if(postID < 1)
this.error(400, "Invalid post ID!");
if(!await PostsHelper.Exists(postID))
this.error(404, "Specified post does not exists!");
return postID;
}
/**
* Get the ID of a post a user has access to
*
* @param name The name of the POST field containing the ID of the target post
*/
public async postPostIDWithAccess(name: string, minLevel: PostAccessLevel = PostAccessLevel.BASIC_ACCESS) : Promise<number> {
const postID = await this.postPostID(name);
if(await PostsHelper.GetAccessLevelFromPostID(this.optionnalUserID, postID) < minLevel)
this.error(401, "Your are not allowed to access this post information!");
return postID;
}
/**
* Get the ID of a comment that the user is allowed to access
*
* @param name The name of the comment field
*/
public async postCommentIDWithAccess(name: string) : Promise<number> {
const commentID = this.postInt(name);
if(!await CommentsHelper.Exists(commentID))
this.error(404, "Specified comment not found!");
const postID = await CommentsHelper.GetAssociatedPost(commentID);
const post = await PostsHelper.GetSingle(postID);
if(await PostsHelper.GetAccessLevel(this.getUserId(), post) == PostAccessLevel.NO_ACCESS)
this.error(401, "You are not allowed to acess this post information!");
return commentID;
}
/**
* Get a virtual directory included in a POST request
*
* @param name The name of the POST variable
* @return The virtual directory, if found as valid
*/
public postVirtualDirectory(name: string) : string {
const dir = this.postString(name);
if(!checkVirtualDirectory(dir))
this.error(401, "Specified directory seems to be invalid!");
return dir;
}
/**
* Get an URL included in a POST request
*
* @param name The name of the POST field containing
* the URL
*/
public postURL(name: string) : string {
const url = this.postString(name);
if(!checkURL(url))
this.error(401, "Specified URL in '"+name+"' seems to be invalid!");
return url;
}
/**
* Get information about an uploaded file
*
* @param name The name of the posted file
*/
private getUploadedFile(name: string) : undefined | UploadedFile {
if(!this.req.files)
return undefined;
if(this.req.files[name] instanceof Array)
this.error(500, "Multiple upload are not supported!");
return <UploadedFile|undefined>this.req.files[name];
}
/**
* Check out whether a file has been included in the request or not
*
* @param name The name of the file to check
*/
public hasFile(name: string) : boolean {
return this.getUploadedFile(name) != undefined;
}
/**
* Save an image in user data directory
*
* @param postField The name of the POST field
* @param folder Target folder in user data directory
* @param maxW Maximum width of the image
* @param maxH Maximum height of the image
*/
public async savePostImage(postField: string, folder: string, maxW: number, maxH: number) : Promise<string> {
const file = this.getUploadedFile(postField);
if(file == undefined)
this.error(400, "File '"+postField+"' is missing in the request !");
const targetUserDataFolder = prepareFileCreation(this.getUserId(), folder);
const targetFilePath = generateNewUserDataFileName(targetUserDataFolder, "png");
const targetSysPath = pathUserData(targetFilePath, true);
// Process image size
let img = sharp(file.data);
const stats = await img.metadata();
if(stats.width > maxW || stats.height > maxH) {
if(stats.width > maxW)
img = img.resize(maxW, Math.floor((stats.height*maxW)/stats.width));
else
img = img.resize(Math.floor((stats.width*maxH)/stats.height), maxH);
}
// Save image
await img.png().toFile(targetSysPath);
return targetFilePath;
}
/**
* Save the file uploaded by a user
*
* @param postField The name of the POST field containing the file
* @param mime_type Required mime type
* @param ext Required file extension
* @param folder The target user folder
*/
public async savePostFile(postField: string, folder: string, ext:string, mime_type: string) : Promise<string> {
const file = this.getUploadedFile(postField);
if(file == undefined)
this.error(400, "File '"+postField+"' is missing in the request !");
if(file.mimetype != mime_type)
this.error(400, "Invalid file mime type (required: " + mime_type+")")
const targetUserDataFolder = prepareFileCreation(this.getUserId(), folder);
const targetFilePath = generateNewUserDataFileName(targetUserDataFolder, ext);
const targetSysPath = pathUserData(targetFilePath, true);
writeFileSync(targetSysPath, file.data, "binary");
return targetFilePath;
}
/**
* Validate API tokens
*
* @throws If the tokens could not be validated
*/
public async checkAPITokens() {
// Extract API name & token from request
const apiName = this.postString("serviceName");
const apiToken = this.postString("serviceToken");
// Validate the client
const client = await APIHelper.GetClient(apiName, apiToken);
if(client == null)
this.error(400, "Client not recognized!");
if(client.domain) {
const allowedOrigin = (conf().force_clients_https ? "https://" : "http://") + client.domain;
const referer = this.req.get("Referer");
if(!referer || !referer.startsWith(allowedOrigin))
this.error(401, "Use of this client is prohibited from this domain!");
this.response.set("Access-Control-Allow-Origin", allowedOrigin);
}
// Save client information for latter access
this.client = client;
}
/**
* Validate user tokens
*
* @param required Specify whether the user MUST be authenticated or not
*/
public async checkUserTokens(required ?: boolean) {
const token1 = this.postString("userToken1", 0, false);
const token2 = this.postString("userToken2", 0, false);
if(token1.length < 1 || token2.length < 1) {
if(required !== false)
this.error(401, "This method requires the user to be signed in!");
return;
}
// Validate user tokens
this.userID = await AccountHelper.GetUserIdFromTokens(this.getClientInfo(), token1, token2);
if(this.userID < 1)
this.error(412, "Please check your login tokens!");
}
/**
* Check the user password included in the request
*
* @param postField The name of the post field
* containing user password
*/
public async needUserPostPassword(postField: string) {
const password = this.postString(postField, 3);
if(!await AccountHelper.CheckUserPassword(this.getUserId(), password))
this.error(401, "Invalid password!");
}
/**
* Get information about API client
*/
public getClientInfo() : APIClient {
if(!this.client)
throw Error("Try to get information about client but client has not been authenticated!");
return this.client;
}
/**
* Get information about current user
*/
public getUserId() : number {
if(this.userID < 1)
throw Error("Trying to get user ID but none are available!");
return this.userID;
}
/**
* Get the ID of the current user (if any)
* or 0 if the user is not signed in
*/
public get optionnalUserID(): number {
return this.userID >= 1 ? this.userID : 0;
}
/**
* Check out whether user is signed in or not
*/
public get signedIn() : boolean {
return this.userID > 0;
}
/**
* Output an error code and throws an error
*
* @param code HTTP Status code
* @param message The message to send
*/
public error(code : number, message : string, should_throw: boolean = true) {
if(this.responseSent)
return;
this.response.status(code).send({
error: {
code: code,
message: message
}
});
this.responseSent = true;
if(should_throw)
throw Error("Could not complete request! ("+ message +")");
}
/**
* Return a successful operation
*
* @param message Message associated to success
*/
public success(message: string = "") {
this.responseSent = true;
this.response.send({
success: message
});
}
/**
* Send some data to the server
*
* @param data The data to send back to the server
*/
public send(data: any) {
this.responseSent = true;
this.response.send(data);
}
}