diff --git a/scripts/parameters/auth.js b/scripts/parameters/auth.js deleted file mode 100644 index 21441c11..00000000 --- a/scripts/parameters/auth.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "auth", - in: "header", - schema: { - type: "string" - }, - required: true, - allowEmptyValue: false, - description: "Authorization Headers." -}; diff --git a/scripts/parameters/direction.js b/scripts/parameters/direction.js deleted file mode 100644 index a2ae641e..00000000 --- a/scripts/parameters/direction.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - name: "direction", - in: "query", - schema: { - type: "string", - enum: [ - "desc", - "asc" - ], - default: "desc" - }, - example: "desc", - allowEmptyValue: true, - description: "Direction to list search results." -}; diff --git a/scripts/parameters/engine.js b/scripts/parameters/engine.js deleted file mode 100644 index addc33f1..00000000 --- a/scripts/parameters/engine.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "engine", - in: "query", - schema: { - type: "string" - }, - example: "1.0.0", - allowEmptyValue: true, - description: "Only show packages compatible with this Pulsar version. Must be a valid Semver." -}; diff --git a/scripts/parameters/fileExtension.js b/scripts/parameters/fileExtension.js deleted file mode 100644 index 08fdecc5..00000000 --- a/scripts/parameters/fileExtension.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "fileExtension", - in: "query", - schema: { - type: "string" - }, - example: "coffee", - allowEmptyValue: true, - description: "File extension of which to only show compatible grammar package's of." -}; diff --git a/scripts/parameters/login.js b/scripts/parameters/login.js deleted file mode 100644 index c2dde762..00000000 --- a/scripts/parameters/login.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: "login", - in: "path", - schema: { - type: "string" - }, - required: true, - allowEmptyValue: false, - example: "confused-Techie", - description: "The User from the URL path." -}; diff --git a/scripts/parameters/packageName.js b/scripts/parameters/packageName.js deleted file mode 100644 index c364d9e6..00000000 --- a/scripts/parameters/packageName.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: "packageName", - in: "path", - schema: { - type: "string" - }, - required: true, - allowEmptyValue: false, - example: "autocomplete-powershell", - description: "The name of the package to return details for. Must be URL escaped." -}; diff --git a/scripts/parameters/page.js b/scripts/parameters/page.js deleted file mode 100644 index 86ae65c3..00000000 --- a/scripts/parameters/page.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - name: "page", - in: "query", - schema: { - type: "number", - minimum: 1, - default: 1 - }, - example: 1, - allowEmptyValue: true, - description: "The page of available results to return." -}; diff --git a/scripts/parameters/query.js b/scripts/parameters/query.js deleted file mode 100644 index ab80a63d..00000000 --- a/scripts/parameters/query.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "q", - in: "query", - schema: { - type: "string" - }, - example: "generic-lsp", - required: true, - description: "Search query." -}; diff --git a/scripts/parameters/service.js b/scripts/parameters/service.js deleted file mode 100644 index db5e8fdf..00000000 --- a/scripts/parameters/service.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "service", - in: "query", - schema: { - type: "string" - }, - example: "autocomplete.watchEditor", - allowEmptyValue: true, - description: "The service of which to filter packages by." -}; diff --git a/scripts/parameters/serviceType.js b/scripts/parameters/serviceType.js deleted file mode 100644 index 2cf69aca..00000000 --- a/scripts/parameters/serviceType.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - name: "serviceType", - in: "query", - schema: { - type: "string", - enum: [ - "consumed", - "provided" - ] - }, - example: "consumed", - allowEmptyValue: true, - description: "Chooses whether to display 'consumer' or 'provider's of the specified 'service'." -}; -// TODO determine if there's a way to indicate this is a required field when -// using the 'service' query param. diff --git a/scripts/parameters/serviceVersion.js b/scripts/parameters/serviceVersion.js deleted file mode 100644 index 6b0f4fce..00000000 --- a/scripts/parameters/serviceVersion.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - name: "serviceVersion", - in: "query", - schema: { - type: "string" - }, - example: "0.0.1", - allowEmptyValue: true, - description: "Filter by a specific version of the 'service'." -}; diff --git a/scripts/parameters/sort.js b/scripts/parameters/sort.js deleted file mode 100644 index 8f5509e6..00000000 --- a/scripts/parameters/sort.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - name: "sort", - in: "query", - schema: { - type: "string", - enum: [ - "downloads", - "created_at", - "updated_at", - "stars", - "relevance" - ], - default: "relevance" - }, - example: "downloads", - allowEmptyValue: true, - description: "Method to sort the results." -}; diff --git a/scripts/parameters/versionName.js b/scripts/parameters/versionName.js deleted file mode 100644 index 132ef28b..00000000 --- a/scripts/parameters/versionName.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - name: "versionName", - in: "path", - schema: { - type: "string" - }, - required: true, - allowEmptyValue: false, - example: "1.0.0", - description: "The version of the package to access." -}; diff --git a/src/context.js b/src/context.js index d9501389..da2fd5aa 100644 --- a/src/context.js +++ b/src/context.js @@ -6,7 +6,7 @@ module.exports = { database: require("./database.js"), webhook: require("./webhook.js"), server_version: require("../package.json").version, - query: require("./query.js"), + query: require("./query_parameters/index.js").logic, vcs: require("./vcs.js"), config: require("./config.js").getConfig(), utils: require("./utils.js"), diff --git a/src/controllers/getOwnersOwnerName.js b/src/controllers/getOwnersOwnerName.js index c60bb523..0dbac251 100644 --- a/src/controllers/getOwnersOwnerName.js +++ b/src/controllers/getOwnersOwnerName.js @@ -23,7 +23,7 @@ module.exports = { params: { page: (context, req) => { return context.query.page(req); }, sort: (context, req) => { return context.query.sort(req); }, - direction: (context, req) => { return context.query.dir(req); }, + direction: (context, req) => { return context.query.direction(req); }, owner: (context, req) => { return context.query.owner(req); } }, diff --git a/src/controllers/getPackages.js b/src/controllers/getPackages.js index b6f10bc7..f986fe98 100644 --- a/src/controllers/getPackages.js +++ b/src/controllers/getPackages.js @@ -27,7 +27,7 @@ module.exports = { params: { page: (context, req) => { return context.query.page(req); }, sort: (context, req) => { return context.query.sort(req); }, - direction: (context, req) => { return context.query.dir(req); }, + direction: (context, req) => { return context.query.direction(req); }, serviceType: (context, req) => { return context.query.serviceType(req); }, service: (context, req) => { return context.query.service(req); }, serviceVersion: (context, req) => { return context.query.serviceVersion(req); }, diff --git a/src/controllers/getPackagesSearch.js b/src/controllers/getPackagesSearch.js index 453e06ed..982b0c2d 100644 --- a/src/controllers/getPackagesSearch.js +++ b/src/controllers/getPackagesSearch.js @@ -19,7 +19,7 @@ module.exports = { params: { sort: (context, req) => { return context.query.sort(req); }, page: (context, req) => { return context.query.page(req); }, - direction: (context, req) => { return context.query.dir(req); }, + direction: (context, req) => { return context.query.direction(req); }, query: (context, req) => { return context.query.query(req); } }, diff --git a/src/controllers/getThemes.js b/src/controllers/getThemes.js index f0ca2b32..0f452cf9 100644 --- a/src/controllers/getThemes.js +++ b/src/controllers/getThemes.js @@ -27,7 +27,7 @@ module.exports = { params: { page: (context, req) => { return context.query.page(req); }, sort: (context, req) => { return context.query.sort(req); }, - direction: (context, req) => { return context.query.dir(req); } + direction: (context, req) => { return context.query.direction(req); } }, /** diff --git a/src/controllers/getThemesSearch.js b/src/controllers/getThemesSearch.js index 97709720..82733f36 100644 --- a/src/controllers/getThemesSearch.js +++ b/src/controllers/getThemesSearch.js @@ -27,7 +27,7 @@ module.exports = { params: { sort: (context, req) => { return context.query.sort(req); }, page: (context, req) => { return context.query.page(req); }, - direction: (context, req) => { return context.query.dir(req); }, + direction: (context, req) => { return context.query.direction(req); }, query: (context, req) => { return context.query.query(req); } }, diff --git a/src/query.js b/src/query.js deleted file mode 100644 index 1199ad8f..00000000 --- a/src/query.js +++ /dev/null @@ -1,366 +0,0 @@ -/** - * @module query - * @desc Home to parsing all query parameters from the `Request` object. Ensuring a valid response. - * While most values will just return their default there are some expecptions: - * engine(): Returns false if not defined, to allow a fast way to determine if results need to be pruned. - */ - -/** - * @function page - * @desc Parser of the Page query parameter. Defaulting to 1. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {number} Returns the valid page provided in the query parameter or 1, as the default. - */ -function page(req) { - const def = 1; - const prov = req.query.page; - - switch (typeof prov) { - case "string": { - const n = parseInt(prov, 10); - return isNaN(prov) ? def : n; - } - - case "number": - return isNaN(prov) ? def : prov; - - default: - return def; - } -} - -/** - * @function sort - * @desc Parser for the 'sort' query parameter. Defaulting usually to downloads. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @param {string} [def="downloads"] - The default provided for sort. Allowing - * The search function to use "relevance" instead of the default "downloads". - * @returns {string} Either the user provided 'sort' query parameter, or the default specified. - */ -function sort(req, def = "downloads") { - // using sort with a default def value of downloads, means when using the generic sort parameter - // it will default to downloads, but if we pass the default, such as during search we can provide - // the default relevance - const valid = ["downloads", "created_at", "updated_at", "stars", "relevance"]; - - const prov = req.query.sort ?? def; - - return valid.includes(prov) ? prov : def; -} - -/** - * @function dir - * @desc Parser for either 'direction' or 'order' query parameter, prioritizing - * 'direction'. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string} The valid direction value from the 'direction' or 'order' - * query parameter. - */ -function dir(req) { - const def = "desc"; - const valid = ["asc", "desc"]; - - // Seems that the autolink headers use order, while documentation uses direction. - // Since we are not sure where in the codebase it uses the other, we will just accept both. - const prov = req.query.direction ?? req.query.order ?? def; - - return valid.includes(prov) ? prov : def; -} - -/** - * @function query - * @desc Checks the 'q' query parameter, trunicating it at 50 characters, and checking simplisticly that - * it is not a malicious request. Returning "" if an unsafe or invalid query is passed. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string} A valid search string derived from 'q' query parameter. Or '' if invalid. - * @implements {pathTraversalAttempt} - */ -function query(req) { - const maxLength = 50; // While package.json names according to NPM can be up to 214 characters, - // for performance on the server and assumed deminishing returns on longer queries, - // this is cut off at 50 as suggested by Digitalone1. - const prov = req.query.q; - - if (typeof prov !== "string") { - return ""; - } - - // If there is a path traversal attach detected return empty query. - // Additionally do not allow strings longer than `maxLength` - return pathTraversalAttempt(prov) ? "" : prov.slice(0, maxLength).trim(); -} - -/** - * @function engine - * @desc Parses the 'engine' query parameter to ensure it's valid, otherwise returning false. - * @param {string} semver - The engine string. - * @returns {string|boolean} Returns the valid 'engine' specified, or if none, returns false. - */ -function engine(semver) { - try { - // Regex inspired by: - // - https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - // - https://regex101.com/r/vkijKf/1/ - // The only difference is that we truncate the check for additional labels because we want to be - // as permissive as possible and need only the first three version numbers. - - const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; - - // Check if it's a valid semver - return semver.match(regex) !== null ? semver : false; - } catch (e) { - return false; - } -} - -/** - * @function auth - * @desc Retrieves Authorization Headers from Request, and Checks for Undefined. - * @param {object} req = The `Request` object inherited from the Express endpoint. - * @returns {string} Returning a valid Authorization Token, or '' if invalid/not found. - */ -function auth(req) { - const token = req.get("Authorization"); - - return token ?? ""; -} - -/** - * @function repo - * @desc Parses the 'repository' query parameter, returning it if valid, otherwise returning ''. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string} Returning the valid 'repository' query parameter, or '' if invalid. - */ -function repo(req) { - const prov = req.query.repository; - - if (prov === undefined) { - return ""; - } - - const re = /^[-a-zA-Z\d][-\w.]{0,213}\/[-a-zA-Z\d][-\w.]{0,213}$/; - - // Ensure req is in the format "owner/repo" and - // owner and repo observe the following rules: - // - less than or equal to 214 characters - // - only URL safe characters (letters, digits, dashes, underscores and/or dots) - // - cannot begin with a dot or an underscore - // - cannot contain a space. - return prov.match(re) !== null ? prov : ""; -} - -/** - * @function tag - * @desc Parses the 'tag' query parameter, returning it if valid, otherwise returning ''. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string} Returns a valid 'tag' query parameter. Or '' if invalid. - */ -function tag(req) { - return typeof req.query.tag !== "string" ? "" : req.query.tag; -} - -/** - * @function rename - * @desc Since this is intended to be returning a boolean value, returns false - * if invalid, otherwise returns true. Checking for mixed captilization. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {boolean} Returns false if invalid, or otherwise returns the boolean value of the string. - */ -function rename(req) { - const prov = req.query.rename; - - if (prov === undefined) { - // Originally it was believed that this query parameter should be handled as - // if it was a text passed boolean. But appears to actually provide the string - // of text the package should be renamed too. - return false; - } - - // Due to the backend already being built in such a way that it will rename - // a package by finding the rename value on it's own, we will still return a - // boolean, but TODO:: this should be fixed in the future. - - if (typeof prov === "string" && prov.length > 0) { - return true; - } - - return false; -} - -/** - * @function packageName - * @desc This function will convert a user provided package name into a safe format. - * It ensures the name is converted to lower case. As is the requirement of all package names. - * @param {object} req - The `Request` Object inherited from the Express endpoint. - * @returns {string} Returns the package name in a safe format that can be worked with further. - */ -function packageName(req) { - return req.params.packageName.toLowerCase(); -} - -/** - * @function pathTraversalAttempt - * @desc Completes some short checks to determine if the data contains a malicious - * path traversal attempt. Returning a boolean indicating if a path traversal attempt - * exists in the data. - * @param {string} data - The data to check for possible malicious data. - * @returns {boolean} True indicates a path traversal attempt was found. False otherwise. - */ -function pathTraversalAttempt(data) { - // This will use several methods to check for the possibility of an attempted path traversal attack. - - // The definitions here are based off GoPage checks. - // https://github.com/confused-Techie/GoPage/blob/main/src/pkg/universalMethods/universalMethods.go - // But we leave out any focused on defended against URL Encoded values, since this has already been decoded. - // const checks = [ - // /\.{2}\//, //unixBackNav - // /\.{2}\\/, //unixBackNavReverse - // /\.{2}/, //unixParentCatchAll - // ]; - - // Combine the 3 regex into one: https://regex101.com/r/CgcZev/1 - const check = /\.{2}(?:[/\\])?/; - return data.match(check) !== null; -} - -/** - * @function login - * @desc Returns the User from the URL Path, otherwise '' - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string} Returns a valid specified user or ''. - */ -function login(req) { - return req.params.login ?? ""; -} - -/** - * @function serviceType - * @desc Returns the service type being requested. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string|boolean} Returns false if the provided value is invalid, or - * nonexistent. Returns `providedServices` if the query is `provided` or returns - * `consumedServices` if the query is `consumed` - */ -function serviceType(req) { - const prov = req.query.serviceType; - - if (prov === undefined) { - return false; - } - - if (prov === "provided") { - return "providedServices"; - } - - if (prov === "consumed") { - return "consumedServices"; - } - - return false; // fallback -} - -/** - * @function serviceVersion - * @desc Returns the version of whatever service is being requested. - * @param {object} req - The `Request` object inherited from the Express Endpoint. - * @returns {string|boolean} Returns false if the provided value is invalid, or - * nonexistant. Returns the version as a string otherwise. - */ -function serviceVersion(req) { - const semver = req.query.serviceVersion; - try { - // Regex matching what's used in query.engine() - const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; - - // Check if it's a valid semver - return semver.match(regex) !== null ? semver : false; - } catch (err) { - return false; - } -} - -/** - * @function service - * @desc Returns the service being requested. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string|boolean} Returns false if the provided value is invalid or - * nonexistent. Returns the service string otherwise. - */ -function service(req) { - return stringValidation(req.query.service); -} - -/** - * @function user - * @param {object} req - The `Request` object inherited from the Express - * endpoint. - * @returns {string|boolean} Returns false if the provided value is invalid or - * nonexistent. Returns the user name otherwise. - */ -function owner (req) { - // Owner accepts the owner as an argument for things like search, - // as well as a path, for the endpoint `/api/owners/:ownerName` - let prov = req.query.owner ?? req.params?.ownerName ?? null; - - if (!stringValidation(prov)) { - return false; - } - if (prov.length === 0) { - return false; - } - return prov; -} - -/** - * @function fileExtension - * @desc Returns the file extension being requested. - * @param {object} req - The `Request` object inherited from the Express endpoint. - * @returns {string|boolean} Returns false if the provided value is invalid, or - * nonexistant. Returns the service string otherwise. - */ -function fileExtension(req) { - return stringValidation(req.query.fileExtension); -} - -// ******************************* -// ******* Query Utilities ******* -// ******************************* - -/** - * @function stringValidation - * @desc Provides a generic Query Utility that validates if a provided value - * is a string, as well as trimming it to the safe max length of query strings, - * while additionally passing it through the Path Traversal Detection function. - * @param {string} value - The value to check - * @returns {string|boolean} Returns false if any check fails, otherwise returns - * the valid string. - */ -function stringValidation(value) { - const maxLength = 50; - const prov = value; - - if (typeof prov !== "string") { - return false; - } - - return pathTraversalAttempt(prov) ? false : prov.slice(0, maxLength).trim(); -} - -module.exports = { - page, - sort, - dir, - query, - engine, - repo, - tag, - rename, - auth, - packageName, - login, - serviceType, - serviceVersion, - service, - owner, - fileExtension, -}; diff --git a/src/query_parameters/auth.js b/src/query_parameters/auth.js new file mode 100644 index 00000000..7b6892ce --- /dev/null +++ b/src/query_parameters/auth.js @@ -0,0 +1,23 @@ +/** + * @function auth + * @desc Retrieves Authorization Headers from Request, and Checks for Undefined. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} Returning a valid Authorization Token, or '' if invalid/not found. + */ +module.exports = { + schema: { + name: "auth", + in: "header", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + description: "Authorization Headers" + }, + logic: (req) => { + const token = req.get("Authorization"); + + return token ?? ""; + } +}; diff --git a/src/query_parameters/direction.js b/src/query_parameters/direction.js new file mode 100644 index 00000000..5c62af57 --- /dev/null +++ b/src/query_parameters/direction.js @@ -0,0 +1,54 @@ +/** + * @function direction + * @desc Parser for either 'direction' or 'order' query parameter, prioritizing + * 'direction'. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} The valid direction value from the 'direction' or 'order' + * query parameter. + */ +module.exports = { + schema: { + name: "multiSchema", // Special name to indicate that within this query + // parameter module, multiple schemas are supported, seperated by name + "direction": { + name: "direction", + in: "query", + schema: { + type: "string", + enum: [ + "desc", + "asc" + ], + default: "desc" + }, + example: "desc", + allowEmptyValue: true, + description: "Direction to list search results." + }, + "order": { + name: "order", + in: "query", + schema: { + type: "string", + enum: [ + "desc", + "asc" + ], + default: "desc" + }, + example: "desc", + allowEmptyValue: true, + description: "Deprecated method to list search results. Use 'direction' instead." + } + }, + logic: (req) => { + const def = "desc"; + const valid = [ "asc", "desc" ]; + + // Seems that the autolink headers use order, while documentation uses direction. + // Since we are not sure where in the codebase it uses the other, we will just accept both. + const prov = req.query.direction ?? req.query.order ?? def; + + return valid.includes(prov) ? prov : def; + } +}; diff --git a/src/query_parameters/engine.js b/src/query_parameters/engine.js new file mode 100644 index 00000000..73bae256 --- /dev/null +++ b/src/query_parameters/engine.js @@ -0,0 +1,35 @@ +/** + * @function engine + * @desc Parses the 'engine' query parameter to ensure it's valid, otherwise returning false. + * @param {string} semver - The engine string. + * @returns {string|boolean} Returns the valid 'engine' specified, or if none, returns false. + */ +module.exports = { + schema: { + name: "engine", + in: "query", + schema: { + type: "string" + }, + example: "1.0.0", + allowEmptyValue: true, + description: "Only show packages compatible with this Pulsar version. Must be a valid Semver." + }, + // TODO: Why does this accept `semver` and not a request object? + logic: (semver) => { + try { + // Regex inspired by: + // - https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + // - https://regex101.com/r/vkijKf/1/ + // The only difference is that we truncate the check for additional labels because we want to be + // as permissive as possible and need only the first three version numbers. + + const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; + + // Check if it's a valid semver + return semver.match(regex) !== null ? semver : false; + } catch (e) { + return false; + } + } +}; diff --git a/src/query_parameters/fileExtension.js b/src/query_parameters/fileExtension.js new file mode 100644 index 00000000..efd4c331 --- /dev/null +++ b/src/query_parameters/fileExtension.js @@ -0,0 +1,24 @@ +/** + * @function fileExtension + * @desc Returns the file extension being requested. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string|boolean} Returns false if the provided value is invalid, or + * nonexistant. Returns the service string otherwise. + */ +const utils = require("./utils.js"); + +module.exports = { + schema: { + name: "fileExtension", + in: "query", + schema: { + type: "string" + }, + example: "coffee", + allowEmptyValue: true, + description: "File extension of which to only show compatible grammar package's of." + }, + logic: (req) => { + return utils.stringValidation(req.query.fileExtension); + } +}; diff --git a/src/query_parameters/index.js b/src/query_parameters/index.js new file mode 100644 index 00000000..93d33de7 --- /dev/null +++ b/src/query_parameters/index.js @@ -0,0 +1,65 @@ +/** + * @module query + * @desc Home to parsing all query parameters from the `Request` object. Ensuring a valid response. + * While most values will just return their default there are some expecptions: + * engine(): Returns false if not defined, to allow a fast way to determine if results need to be pruned. + */ + +const auth = require("./auth.js"); +const direction = require("./direction.js"); +const engine = require("./engine.js"); +const fileExtension = require("./fileExtension.js"); +const login = require("./login.js"); +const owner = require("./owner.js"); +const packageName = require("./packageName.js"); +const page = require("./page.js"); +const query = require("./query.js"); +const rename = require("./rename.js"); +const repo = require("./repo.js"); +const service = require("./service.js"); +const serviceType = require("./serviceType.js"); +const serviceVersion = require("./serviceVersion.js"); +const sort = require("./sort.js"); +const tag = require("./tag.js"); +const versionName = require("./versionName.js"); + +module.exports = { + logic: { + auth: auth.logic, + direction: direction.logic, + engine: engine.logic, + fileExtension: fileExtension.logic, + login: login.logic, + owner: owner.logic, + packageName: packageName.logic, + page: page.logic, + query: query.logic, + rename: rename.logic, + repo: repo.logic, + service: service.logic, + serviceType: serviceType.logic, + serviceVersion: serviceVersion.logic, + sort: sort.logic, + tag: tag.logic, + versionName: versionName.logic, + }, + schema: { + auth: auth.schema, + direction: direction.schema, + engine: engine.schema, + fileExtension: fileExtension.schema, + login: login.schema, + owner: owner.schema, + packageName: packageName.schema, + page: page.schema, + query: query.schema, + rename: rename.schema, + repo: repo.schema, + service: service.schema, + serviceType: serviceType.schema, + serviceVersion: serviceVersion.schema, + sort: sort.schema, + tag: tag.schema, + versionName: versionName.schema, + } +}; diff --git a/src/query_parameters/login.js b/src/query_parameters/login.js new file mode 100644 index 00000000..4b001f72 --- /dev/null +++ b/src/query_parameters/login.js @@ -0,0 +1,23 @@ +/** + * @function login + * @desc Returns the User from the URL Path, otherwise '' + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} Returns a valid specified user or ''. + */ + +module.exports = { + schema: { + name: "login", + in: "path", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + example: "confused-Techie", + description: "The User from the URL Path" + }, + logic: (req) => { + return req.params.login ?? ""; + } +}; diff --git a/src/query_parameters/owner.js b/src/query_parameters/owner.js new file mode 100644 index 00000000..a0140746 --- /dev/null +++ b/src/query_parameters/owner.js @@ -0,0 +1,50 @@ +/** + * @function user + * @param {object} req - The `Request` object inherited from the Express + * endpoint. + * @returns {string|boolean} Returns false if the provided value is invalid or + * nonexistent. Returns the user name otherwise. + */ + +const utils = require("./utils.js"); + +module.exports = { + schema: { + name: "multiSchema", // Special name to indicate multi support + "owner": { + name: "owner", + in: "query", + schema: { + type: "string" + }, + example: "pulsar-edit", + allowEmptyValue: false, + required: false, + description: "Owner to filter results by." + }, + "ownerName": { + name: "ownerName", + in: "path", + schema: { + type: "string" + }, + example: "pulsar-edit", + allowEmptyValue: false, + required: true, + description: "Owner of packages to retreive." + } + }, + logic: (req) => { + // Owner accepts the owner as an argument for things like search, + // as well as a path, for the endpoint `/api/owners/:ownerName` + let prov = req.query.owner ?? req.params?.ownerName ?? null; + + if (!utils.stringValidation(prov)) { + return false; + } + if (prov.length === 0) { + return false; + } + return prov; + } +}; diff --git a/src/query_parameters/packageName.js b/src/query_parameters/packageName.js new file mode 100644 index 00000000..cbe82c8f --- /dev/null +++ b/src/query_parameters/packageName.js @@ -0,0 +1,23 @@ +/** + * @function packageName + * @desc This function will convert a user provided package name into a safe format. + * It ensures the name is converted to lower case. As is the requirement of all package names. + * @param {object} req - The `Request` Object inherited from the Express endpoint. + * @returns {string} Returns the package name in a safe format that can be worked with further. + */ +module.exports = { + schema: { + name: "packageName", + in: "path", + schema: { + type: "string" + }, + required: true, + allowEmptyValue: false, + example: "autocomplete-powershell", + description: "The name of the package to reutrn details for. Must be URL escaped." + }, + logic: (req) => { + return req.params.packageName.toLowerCase(); + } +}; diff --git a/src/query_parameters/page.js b/src/query_parameters/page.js new file mode 100644 index 00000000..2d89e792 --- /dev/null +++ b/src/query_parameters/page.js @@ -0,0 +1,39 @@ +/** + * @function page + * @desc Parser of the Page query parameter. Defaulting to 1. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {number} Returns the valid page provided in the query parameter or 1, as the default. + */ + +module.exports = { + schema: { + name: "page", + in: "query", + schema: { + type: "number", + minimum: 1, + default: 1 + }, + example: 1, + allowEmptyValue: true, + required: false, + description: "The page of available results to return." + }, + logic: (req) => { + const def = 1; + const prov = req.query.page; + + switch (typeof prov) { + case "string": { + const n = parseInt(prov, 10); + return isNaN(prov) ? def : n; + } + + case "number": + return isNaN(prov) ? def : prov; + + default: + return def; + } + } +}; diff --git a/src/query_parameters/query.js b/src/query_parameters/query.js new file mode 100644 index 00000000..03c03f87 --- /dev/null +++ b/src/query_parameters/query.js @@ -0,0 +1,36 @@ +const utils = require("./utils.js"); +/** + * @function query + * @desc Checks the 'q' query parameter, trunicating it at 50 characters, and checking simplisticly that + * it is not a malicious request. Returning "" if an unsafe or invalid query is passed. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} A valid search string derived from 'q' query parameter. Or '' if invalid. + * @implements {pathTraversalAttempt} + */ + +module.exports = { + schema: { + name: "q", + in: "query", + schema: { + type: "string" + }, + example: "generic-lsp", + required: true, + description: "Search Query" + }, + logic: (req) => { + const maxLength = 50; // While package.json names according to NPM can be up to 214 characters, + // for performance on the server and assumed deminishing returns on longer queries, + // this is cut off at 50 as suggested by Digitalone1. + const prov = req.query.q; + + if (typeof prov !== "string") { + return ""; + } + + // If there is a path traversal attach detected return empty query. + // Additionally do not allow strings longer than `maxLength` + return utils.pathTraversalAttempt(prov) ? "" : prov.slice(0, maxLength).trim(); + } +}; diff --git a/src/query_parameters/rename.js b/src/query_parameters/rename.js new file mode 100644 index 00000000..b6670b5a --- /dev/null +++ b/src/query_parameters/rename.js @@ -0,0 +1,41 @@ +/** + * @function rename + * @desc Since this is intended to be returning a boolean value, returns false + * if invalid, otherwise returns true. Checking for mixed captilization. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {boolean} Returns false if invalid, or otherwise returns the boolean value of the string. + */ + +module.exports = { + schema: { + name: "rename", + in: "query", + schema: { + type: "string" + }, + example: "new-package-name", + allowEmptyValue: false, + required: false, + description: "The new package name to rename to, if applicable." + }, + logic: (req) => { + const prov = req.query.rename; + + if (prov === undefined) { + // Originally it was believed that this query parameter should be handled as + // if it was a text passed boolean. But appears to actually provide the string + // of text the package should be renamed too. + return false; + } + + // Due to the backend already being built in such a way that it will rename + // a package by finding the rename value on it's own, we will still return a + // boolean, but TODO:: this should be fixed in the future. + + if (typeof prov === "string" && prov.length > 0) { + return true; + } + + return false; + } +}; diff --git a/src/query_parameters/repo.js b/src/query_parameters/repo.js new file mode 100644 index 00000000..563a2b59 --- /dev/null +++ b/src/query_parameters/repo.js @@ -0,0 +1,37 @@ +/** + * @function repo + * @desc Parses the 'repository' query parameter, returning it if valid, otherwise returning ''. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} Returning the valid 'repository' query parameter, or '' if invalid. + */ + +module.exports = { + schema: { + name: "repository", + in: "query", + schema: { + type: "string" + }, + example: "pulsar-edit/pulsar", + allowEmptyValue: false, + required: true, + description: "Repository to publish." + }, + logic: (req) => { + const prov = req.query.repository; + + if (prov === undefined) { + return ""; + } + + const re = /^[-a-zA-Z\d][-\w.]{0,213}\/[-a-zA-Z\d][-\w.]{0,213}$/; + + // Ensure req is in the format "owner/repo" and + // owner and repo observe the following rules: + // - less than or equal to 214 characters + // - only URL safe characters (letters, digits, dashes, underscores and/or dots) + // - cannot begin with a dot or an underscore + // - cannot contain a space. + return prov.match(re) !== null ? prov : ""; + } +}; diff --git a/src/query_parameters/service.js b/src/query_parameters/service.js new file mode 100644 index 00000000..5060d8ed --- /dev/null +++ b/src/query_parameters/service.js @@ -0,0 +1,24 @@ +/** + * @function service + * @desc Returns the service being requested. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string|boolean} Returns false if the provided value is invalid or + * nonexistent. Returns the service string otherwise. + */ +const utils = require("./utils.js"); + +module.exports = { + schema: { + name: "service", + in: "query", + schema: { + type: "string" + }, + example: "autocomplete.watchEditor", + allowEmptyValue: true, + description: "The service of which to filter packages by" + }, + logic: (req) => { + return utils.stringValidation(req.query.service); + } +}; diff --git a/src/query_parameters/serviceType.js b/src/query_parameters/serviceType.js new file mode 100644 index 00000000..196dabd3 --- /dev/null +++ b/src/query_parameters/serviceType.js @@ -0,0 +1,44 @@ +/** + * @function serviceType + * @desc Returns the service type being requested. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string|boolean} Returns false if the provided value is invalid, or + * nonexistent. Returns `providedServices` if the query is `provided` or returns + * `consumedServices` if the query is `consumed` + */ + +module.exports = { + schema: { + name: "serviceType", + in: "query", + schema: { + type: "string", + enum: [ + "consumed", + "provided" + ] + }, + example: "consumed", + allowEmptyValue: true, + deescription: "Choos whether to display 'consumer' or 'providers' of the specified service." + }, + logic: (req) => { + // TODO determine if there's a way to indicate this is a required + // field moo + const prov = req.query.serviceType; + + if (prov === undefined) { + return false; + } + + if (prov === "provided") { + return "providedServices"; + } + + if (prov === "consumed") { + return "consumedServices"; + } + + return false; // fallback + } +}; diff --git a/src/query_parameters/serviceVersion.js b/src/query_parameters/serviceVersion.js new file mode 100644 index 00000000..9907cd54 --- /dev/null +++ b/src/query_parameters/serviceVersion.js @@ -0,0 +1,32 @@ +/** + * @function serviceVersion + * @desc Returns the version of whatever service is being requested. + * @param {object} req - The `Request` object inherited from the Express Endpoint. + * @returns {string|boolean} Returns false if the provided value is invalid, or + * nonexistant. Returns the version as a string otherwise. + */ + +module.exports = { + schema: { + name: "serviceVersion", + in: "query", + schema: { + type: "string" + }, + example: "0.0.1", + allowEmptyValue: true, + description: "Filter by a specific version of the 'service'" + }, + logic: (req) => { + const semver = req.query.serviceVersion; + try { + // Regex matching what's used in query.engine() + const regex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/; + + // Check if it's a valid semver + return semver.match(regex) !== null ? semver : false; + } catch (err) { + return false; + } + } +}; diff --git a/src/query_parameters/sort.js b/src/query_parameters/sort.js new file mode 100644 index 00000000..50361216 --- /dev/null +++ b/src/query_parameters/sort.js @@ -0,0 +1,41 @@ +/** + * @function sort + * @desc Parser for the 'sort' query parameter. Defaulting usually to downloads. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @param {string} [def="downloads"] - The default provided for sort. Allowing + * The search function to use "relevance" instead of the default "downloads". + * @returns {string} Either the user provided 'sort' query parameter, or the default specified. + */ + +module.exports = { + schema: { + name: "sort", + in: "query", + schema: { + type: "string", + enum: [ + "downloads", + "created_at", + "updated_at", + "stars", + "relevance" + ], + default: "downloads" + }, + example: "downloads", + required: false, + allowEmptyValue: false, + description: "Value to sort search results by." + }, + logic: (req, def = "downloads") => { + // TODO: Determine if allowing `def` value here makes any sense still + // using sort with a default def value of downloads, means when using the generic sort parameter + // it will default to downloads, but if we pass the default, such as during search we can provide + // the default relevance + const valid = ["downloads", "created_at", "updated_at", "stars", "relevance"]; + + const prov = req.query.sort ?? def; + + return valid.includes(prov) ? prov : def; + } +}; diff --git a/src/query_parameters/tag.js b/src/query_parameters/tag.js new file mode 100644 index 00000000..5d4aa059 --- /dev/null +++ b/src/query_parameters/tag.js @@ -0,0 +1,23 @@ +/** + * @function tag + * @desc Parses the 'tag' query parameter, returning it if valid, otherwise returning ''. + * @param {object} req - The `Request` object inherited from the Express endpoint. + * @returns {string} Returns a valid 'tag' query parameter. Or '' if invalid. + */ + +module.exports = { + schema: { + name: "tag", + in: "query", + schema: { + type: "string" + }, + example: "TODO", + allowEmptyValue: false, + required: false, + description: "TODO" + }, + logic: (req) => { + return typeof req.query.tag !== "string" ? "" : req.query.tag; + } +}; diff --git a/src/query_parameters/utils.js b/src/query_parameters/utils.js new file mode 100644 index 00000000..efb2a5a8 --- /dev/null +++ b/src/query_parameters/utils.js @@ -0,0 +1,49 @@ +/** + * @function stringValidation + * @desc Provides a generic Query Utility that validates if a provided value + * is a string, as well as trimming it to the safe max length of query strings, + * while additionally passing it through the Path Traversal Detection function. + * @param {string} value - The value to check + * @returns {string|boolean} Returns false if any check fails, otherwise returns + * the valid string. + */ +function stringValidation(value) { + const maxLength = 50; + const prov = value; + + if (typeof prov !== "string") { + return false; + } + + return pathTraversalAttempt(prov) ? false : prov.slice(0, maxLength).trim(); +} + +/** + * @function pathTraversalAttempt + * @desc Completes some short checks to determine if the data contains a malicious + * path traversal attempt. Returning a boolean indicating if a path traversal attempt + * exists in the data. + * @param {string} data - The data to check for possible malicious data. + * @returns {boolean} True indicates a path traversal attempt was found. False otherwise. + */ +function pathTraversalAttempt(data) { + // This will use several methods to check for the possibility of an attempted path traversal attack. + + // The definitions here are based off GoPage checks. + // https://github.com/confused-Techie/GoPage/blob/main/src/pkg/universalMethods/universalMethods.go + // But we leave out any focused on defended against URL Encoded values, since this has already been decoded. + // const checks = [ + // /\.{2}\//, //unixBackNav + // /\.{2}\\/, //unixBackNavReverse + // /\.{2}/, //unixParentCatchAll + // ]; + + // Combine the 3 regex into one: https://regex101.com/r/CgcZev/1 + const check = /\.{2}(?:[/\\])?/; + return data.match(check) !== null; +} + +module.exports = { + stringValidation, + pathTraversalAttempt, +} diff --git a/src/query_parameters/versionName.js b/src/query_parameters/versionName.js new file mode 100644 index 00000000..6f6e35ce --- /dev/null +++ b/src/query_parameters/versionName.js @@ -0,0 +1,16 @@ +const engine = require("./engine.js").logic; + +module.exports = { + schema: { + name: "versionName", + in: "path", + schema: { + type: 'string"' + }, + required: true, + allowEmptyValue: false, + example: "1.0.0", + description: "The version of the package to access" + }, + logic: engine +}; diff --git a/src/vcs.js b/src/vcs.js index 51acc3d0..de5309d5 100644 --- a/src/vcs.js +++ b/src/vcs.js @@ -6,7 +6,7 @@ * function. */ -const query = require("./query.js"); +const query = require("./query_parameters/index.js").logic; const utils = require("./utils.js"); const PackageObject = require("./PackageObject.js"); const GitHub = require("./vcs_providers/github.js"); diff --git a/tests/unit/query.test.js b/tests/unit/query.test.js index bb8ab92d..cb907a12 100644 --- a/tests/unit/query.test.js +++ b/tests/unit/query.test.js @@ -1,4 +1,4 @@ -const query = require("../../src/query.js"); +const query = require("../../src/query_parameters/index.js").logic; // Page Testing @@ -40,7 +40,7 @@ const dirCases = [ describe("Verify Direction Query Returns", () => { test.each(dirCases)("Given %o Returns %p", (arg, result) => { - expect(query.dir(arg)).toBe(result); + expect(query.direction(arg)).toBe(result); }); }); @@ -52,7 +52,7 @@ const orderCases = [ describe("Verify Order Query Returns", () => { test.each(orderCases)("Given %o Returns %p", (arg, result) => { - expect(query.dir(arg)).toBe(result); + expect(query.direction(arg)).toBe(result); }); });