diff --git a/dist/lib/base-tools.d.ts b/dist/lib/base-tools.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c090e8296ad1c9f590e7a98b9cd42e32b560b8f --- /dev/null +++ b/dist/lib/base-tools.d.ts @@ -0,0 +1,29 @@ +/// +import EventEmitter from 'events'; +import { configOptionType, resolvedPayloadType } from './types'; +declare class BaseTools extends EventEmitter { + protected options: configOptionType; + constructor(options: configOptionType); + /** + * parsing the raw heading and keep them in original format (no lower case) + * @param {*} req request object + * @return {object} + */ + protected parseHeader(req: any): any; + /** + * Extract the json payload + * @param {object} req the request Object + * @return {object} Promise + */ + protected parsePayload(req: any): Promise; + /** + * @param {object} res the respond object unable to get a correct type IncomingMessage? + * @param {string} err error string, this might or might not have, therefore make it optional + * @return {void} nothing + */ + protected resError(res: any, err?: any): void; + protected getUrlPath(req: any): string; + protected validate(req: any): boolean; + protected handler(req: any, res: any, verifyFn: any): Promise; +} +export { BaseTools, configOptionType }; diff --git a/dist/lib/base-tools.js b/dist/lib/base-tools.js new file mode 100644 index 0000000000000000000000000000000000000000..959b5ae34509d8306c76ff4e05db6772eaa1f0de --- /dev/null +++ b/dist/lib/base-tools.js @@ -0,0 +1,92 @@ +// src/lib/base-tools +import EventEmitter from 'events'; +import { debugFn } from './helpers'; +const debug = debugFn('git-webhook-ci:base-tools'); +class BaseTools extends EventEmitter { + // class constructor + constructor(options) { + super(); + this.options = options; + } + /** + * parsing the raw heading and keep them in original format (no lower case) + * @param {*} req request object + * @return {object} + */ + parseHeader(req) { + const headers = req.rawHeaders; + const ctn = headers.length; + const h = {}; + for (let i = 0; i < ctn; i += 2) { + h[headers[i]] = headers[i + 1]; + } + return h; + } + /** + * Extract the json payload + * @param {object} req the request Object + * @return {object} Promise + */ + parsePayload(req) { + debug('call parsePayload'); + return new Promise((resolver, rejecter) => { + // V.2 here we also need to parse the header and add to the json + // and the result object will become { payload: Object, header: Object } + const header = this.parseHeader(req); + let body = []; + req + .on('data', (chunk) => { + body.push(chunk); + }) + .on('end', () => { + // should catch error here as well + try { + const json = Buffer.concat(body).toString(); + resolver({ + header, + payload: JSON.parse(json) + }); + } + catch (e) { + rejecter(e); + } + }) + .on('error', rejecter); // just throw the rejecter in to handle it + }); + } + /** + * @param {object} res the respond object unable to get a correct type IncomingMessage? + * @param {string} err error string, this might or might not have, therefore make it optional + * @return {void} nothing + */ + resError(res, err) { + res.writeHead(400, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ + error: err + })); + this.emit('error', new Error(err)); + } + // simple clean up method to get the url path + getUrlPath(req) { + return req.url.split('?').shift(); + } + // put the validate method here + validate(req) { + return req.method === 'POST' && this.getUrlPath(req) === this.options.path; + } + // this will be overwritten by the child + handler(req, res, verifyFn) { + if (!this.validate(req)) { + debug(req.url, this.options.path); + return Promise.reject(new Error(`Path validate failed`)); + } + return this.parsePayload(req) + .then(obj => { + return verifyFn(obj, this.options) // we move the this.options as param to get round the scope problem + .catch((err) => { + this.resError(res, err); + }); + }); + } +} +export { BaseTools }; diff --git a/dist/lib/helpers.d.ts b/dist/lib/helpers.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..85ec73155591ab3afdb0e1c3f15b80fd1f3072d8 --- /dev/null +++ b/dist/lib/helpers.d.ts @@ -0,0 +1,4 @@ +import debug from 'debug'; +export declare const debugFn: (name: string) => debug.Debugger; +export declare const getTimestamp: () => number; +export declare const getRandomInt: (min: number, max: number) => number; diff --git a/dist/lib/helpers.js b/dist/lib/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..51cfb954d005daff5749130281d84a54bb378819 --- /dev/null +++ b/dist/lib/helpers.js @@ -0,0 +1,8 @@ +// src/lib/helpers.ts +import debug from 'debug'; +// this way we don't need to import everywhere and get back the debug namespace +export const debugFn = (name) => debug(name); +// wrapper to get a timestamp +export const getTimestamp = () => Date.now(); +// return a random number between min and max +export const getRandomInt = (min, max) => (Math.floor(Math.random() * (max - min + 1)) + min); diff --git a/dist/lib/index.d.ts b/dist/lib/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..2fbd5ff93c080f2cb3e3c9c99b3d93020e82f228 --- /dev/null +++ b/dist/lib/index.d.ts @@ -0,0 +1,4 @@ +import createServer from './server'; +export { createServer }; +export { debugFn } from './helpers'; +export { BaseTools, configOptionType } from './base-tools'; diff --git a/dist/lib/index.js b/dist/lib/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bfaaf81ba2e3e68912729a69a18f878a43a85e67 --- /dev/null +++ b/dist/lib/index.js @@ -0,0 +1,6 @@ +// src/lib/index.ts +// just group the export together +import createServer from './server'; +export { createServer }; +export { debugFn } from './helpers'; +export { BaseTools } from './base-tools'; diff --git a/dist/lib/option.d.ts b/dist/lib/option.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5c5196f115475ca8a4f2efe6d3804ec80fa9552 --- /dev/null +++ b/dist/lib/option.d.ts @@ -0,0 +1 @@ +export declare const defaultOptions: any; diff --git a/dist/lib/option.js b/dist/lib/option.js new file mode 100644 index 0000000000000000000000000000000000000000..45c384f1a6cfc66c47018ef1d5c8d5471db479c2 --- /dev/null +++ b/dist/lib/option.js @@ -0,0 +1,14 @@ +// src/lib/option.ts +// base config option +// @NOTE here is a problem github use /payload as default now +// basically all the required options +export const defaultOptions = { + port: 8081, + provider: 'gitee', + path: '/webhook', + branch: 'refs/heads/master', + cmd: 'git pull origin master --no-edit', + pwd: process.cwd(), + env: process.env, + error: () => { } // just a placeholder +}; diff --git a/dist/lib/server.d.ts b/dist/lib/server.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..248d21ad3b970a7cc56d0e940e41c82053a3f5f9 --- /dev/null +++ b/dist/lib/server.d.ts @@ -0,0 +1,9 @@ +/** + * super simple http server using build-in node http + * @param {function} callback + * @param {object} config + * @param {function} debug + * @return {http.Server} + */ +declare function createBareServer(callback: any, config?: any, debug?: any): any; +export default createBareServer; diff --git a/dist/lib/server.js b/dist/lib/server.js new file mode 100644 index 0000000000000000000000000000000000000000..765d899a59602999f0ae86c46d73e17495a5f9a7 --- /dev/null +++ b/dist/lib/server.js @@ -0,0 +1,26 @@ +// src/lib/server.ts +// create a barebone server +import { createServer } from 'http'; +/** + * super simple http server using build-in node http + * @param {function} callback + * @param {object} config + * @param {function} debug + * @return {http.Server} + */ +function createBareServer(callback, config = {}, debug = () => { }) { + const srv = createServer(callback); + if (process.env.NODE_ENV === 'test') { + return srv; + } + if (!config.port) { + throw new Error(`Expect a config.port property!`); + } + return srv.listen(config.port, () => { + try { + debug(`${config.provider} webhook server start @ ${config.port}`); + } + catch (e) { } + }); +} +export default createBareServer; diff --git a/dist/lib/types.d.ts b/dist/lib/types.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..e6710bd7efa12d5c63e6ad112090b503b0ee4215 --- /dev/null +++ b/dist/lib/types.d.ts @@ -0,0 +1,17 @@ +declare type configOptionType = { + port: number; + secret: string; + provider: string; + path: string; + branch: string; + cmd: string | any; + pwd?: string; + env?: any; + error?: any; + inited?: boolean; +}; +declare type resolvedPayloadType = { + header: any; + payload: any; +}; +export { configOptionType, resolvedPayloadType }; diff --git a/dist/lib/types.js b/dist/lib/types.js new file mode 100644 index 0000000000000000000000000000000000000000..5ab29538286a421cd2e9c2162d763d8c9ce90d88 --- /dev/null +++ b/dist/lib/types.js @@ -0,0 +1,3 @@ +// src/lib/config-option-type +// just setup a type for the options to use +export {}; diff --git a/dist/main.d.ts b/dist/main.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..fca53757c26a98523e173f59fc60f3fbefaec003 --- /dev/null +++ b/dist/main.d.ts @@ -0,0 +1,6 @@ +/** + * Finally the main method + * @param {object} config + * @return {function} for calls + */ +export declare function gitWebhookCi(options: any): any; diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000000000000000000000000000000000000..916f35357755daf6e5d9bbd699ffd6b8a3c0664d --- /dev/null +++ b/dist/main.js @@ -0,0 +1,49 @@ +// src/main.ts it was the main file so rename to main and the function rename as well +import { spawn } from 'child_process'; +import { getProvider } from './provider'; +import { defaultOptions } from './lib/option'; +import { debugFn } from './lib'; +const debug = debugFn('git-webhook-ci:main'); +/** + * create a callback to execute + * @param {object} opt --> need this to pass the env and pwd to spawn + * @param {string} cmd + */ +function createCallback(cmd) { + // the signature just matching the cmd callback and create a problem here + return function callback(_, opt) { + const ps = spawn(cmd[0], cmd.filter((_, i) => i > 0), opt); + ps.stdout.on('data', data => { + debug("cmd stdout:", data); + }); + ps.stderr.on('data', data => { + debug("cmd stderr:", data); + }); + ps.on('end', code => { + debug(`cmd exited with ${code}`); + }); + }; +} +/** + * Finally the main method + * @param {object} config + * @return {function} for calls + */ +export function gitWebhookCi(options) { + // yeah type safe ... you still need to do validation + if (typeof options !== 'object') { + throw new Error('Expecting options to be an object'); + } + const config = Object.assign({}, defaultOptions, options); + if (!config.secret || config.secret === '') { + throw new Error('You must provide the secret!'); + } + if (typeof config.cmd !== 'string' && typeof config.cmd !== 'function') { + throw new Error('Cmd must be a string or a function!'); + } + debug(config); + const createHandler = getProvider(config.provider); + // Return without Promise, because there is no need to + return createHandler(config, typeof config.cmd === 'function' ? config.cmd : createCallback(config.cmd.split(' ')), config.error // this is the error Handler + ); +} diff --git a/dist/provider/gitee/gitee-handler.d.ts b/dist/provider/gitee/gitee-handler.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e1ad95829326f8d022fc0ab486d8c7d2745f93a --- /dev/null +++ b/dist/provider/gitee/gitee-handler.d.ts @@ -0,0 +1,25 @@ +import { BaseTools, configOptionType } from '../../lib/base-tools'; +export declare class GiteeHandler extends BaseTools { + constructor(options: configOptionType); + /** + * Main method, the only one that get call + * @param {object} req the request + * @param {object} res the respond + * @param {function} callback res with 404 + * @return {null} nothing + */ + handler(req: any, res: any, callback: any): any; + /** + * Verify the password field + * @param {object} payload Content + * @return {object} promise + */ + private verify; + /** + * @param {object} req the request + * @param {object} res the respond + * @param {object} result the payload + * @return {null} nothing + */ + private resSuccess; +} diff --git a/dist/provider/gitee/gitee-handler.js b/dist/provider/gitee/gitee-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..d1f0cd5fcb6410a9d0a5482905a9e47fb7e67bda --- /dev/null +++ b/dist/provider/gitee/gitee-handler.js @@ -0,0 +1,65 @@ +// src/provider/gitee/gitee-class.ts +import { BaseTools } from '../../lib/base-tools'; +import { verifyHandler } from './verify'; +import { debugFn } from '../../lib/helpers'; +const debug = debugFn('git-webhook-ci:gitee:handler'); +export class GiteeHandler extends BaseTools { + constructor(options) { + super(options); + } + /** + * Main method, the only one that get call + * @param {object} req the request + * @param {object} res the respond + * @param {function} callback res with 404 + * @return {null} nothing + */ + handler(req, res, callback) { + debug(`got call here`); + return super.handler(req, res, this.verify) + .then(result => { + this.resSuccess(req, res, result); + }) + .catch(callback); + } + /** + * Verify the password field + * @param {object} payload Content + * @return {object} promise + */ + verify(obj, opt) { + return new Promise((resolver, rejecter) => { + const { header, payload } = obj; + if (verifyHandler(header, opt.secret)) { + resolver(payload); + } + else { + rejecter(new Error('Gitee verify failed')); + } + }); + } + /** + * @param {object} req the request + * @param {object} res the respond + * @param {object} result the payload + * @return {null} nothing + */ + resSuccess(req, res, result) { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end('{"ok":true}'); + // Check the result if this is what we wanted + if (result.hook_name === 'push_hooks') { // @TODO check if this is still correct + this.emit('push', { + payload: result, + host: req.headers.host, + event: result.hook_name + }); + } + else { + this.emit('error', { + msg: 'Not the event we are expecting', + event: result.hook_name + }); + } + } +} diff --git a/dist/provider/gitee/index.d.ts b/dist/provider/gitee/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..a42b3f4f6b67742ca84ba80a6bfda4b529903bbd --- /dev/null +++ b/dist/provider/gitee/index.d.ts @@ -0,0 +1,10 @@ +import { configOptionType } from '../../lib'; +/** + * The main method to handle the server create and run the whole service for gitee + * @param {configOptionType} config for the overall setup of the system + * @param {function} callback + * @param {function} errorHandler optional + * @return {http server instance} + */ +declare function createGiteeServer(config: configOptionType, callback: any, errorHandler?: any): any; +export default createGiteeServer; diff --git a/dist/provider/gitee/index.js b/dist/provider/gitee/index.js new file mode 100644 index 0000000000000000000000000000000000000000..556df4f32f4c3f710e89671c38e82416c82a43e3 --- /dev/null +++ b/dist/provider/gitee/index.js @@ -0,0 +1,41 @@ +// src/provider/gitee/index.ts +// the actual execution +import { GiteeHandler } from './gitee-handler'; +import { createServer, debugFn } from '../../lib'; +const debug = debugFn('git-webhook-ci:gitee'); +/** + * The main method to handle the server create and run the whole service for gitee + * @param {configOptionType} config for the overall setup of the system + * @param {function} callback + * @param {function} errorHandler optional + * @return {http server instance} + */ +function createGiteeServer(config, callback, errorHandler = () => { }) { + const gitee = new GiteeHandler(config); + // just debug it out + gitee.on('error', (err) => { + debug('ERROR', err); + errorHandler(err); + }); + gitee.on('push', (result) => { + const ref = result.payload.ref; // ref is the branch name + if (config.branch === '*' || config.branch === ref) { + callback(result, config, ref); + } + else { + errorHandler(ref); + debug('Gitee webhook is not expecting this branch', ref); + } + }); + // return the server instance + return createServer((req, res) => { + debug(`server callback executed`); + gitee.handler(req, res, (err) => { + debug('The url got called! [%s]', req.url, err); + errorHandler(req.url, err); + res.statusCode = 404; + res.end('-- no such location --'); + }); + }, config, debug); +} +export default createGiteeServer; diff --git a/dist/provider/gitee/verify.d.ts b/dist/provider/gitee/verify.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..659f63b4580f5eeecd3ca08adbea4352d91f7779 --- /dev/null +++ b/dist/provider/gitee/verify.d.ts @@ -0,0 +1,13 @@ +/** + * create the secret key to compare + * @param {string} secretKey the secret key set during sestting up the webhook + * @param {number} timestamp this timestamp send from the git provider server + */ +export declare function getToken(secretKey: string, timestamp: number): string; +/** + * gitee has it's own payload structure therefore we need to check if we have those things in the header + * @param {*} header the parsed header from req + * @param {string} secretKey the secret key provided when setup the webhook + * @return {boolean} + */ +export declare function verifyHandler(header: any, secretKey: string): boolean; diff --git a/dist/provider/gitee/verify.js b/dist/provider/gitee/verify.js new file mode 100644 index 0000000000000000000000000000000000000000..8ca14911e279013355d7ff2c169bf73da0b436d0 --- /dev/null +++ b/dist/provider/gitee/verify.js @@ -0,0 +1,40 @@ +// src/provider/gitee/secret.ts +// see here: https://gitee.com/help/articles/4290#article-header3 +import { createHmac } from 'crypto'; +// use the debug to find out what went wrong +import { debugFn } from '../../lib/helpers'; +const debug = debugFn('git-webhook-ci:gitee:verify'); +/** + * create the secret key to compare + * @param {string} secretKey the secret key set during sestting up the webhook + * @param {number} timestamp this timestamp send from the git provider server + */ +export function getToken(secretKey, timestamp) { + const secret_enc = Buffer.from(secretKey, 'utf8'); + const string_to_sign = `${timestamp}\n${secret_enc}`; + const hmac = createHmac('sha256', secret_enc); + const data = hmac.update(string_to_sign); + return encodeURIComponent(data.digest('base64')); +} +/** + * gitee has it's own payload structure therefore we need to check if we have those things in the header + * @param {*} header the parsed header from req + * @param {string} secretKey the secret key provided when setup the webhook + * @return {boolean} + */ +export function verifyHandler(header, secretKey) { + if (header['User-Agent'] === 'git-oschina-hook') { + debug('User-Agent passed', header['User-Agent']); + const expected = ['X-Gitee-Token', 'X-Gitee-Timestamp', 'X-Gitee-Event'].filter(key => header[key] !== undefined).length; + if (expected === 3) { + debug('Expected header passed'); + if (header['X-Gitee-Token'] === getToken(secretKey, parseInt(header['X-Gitee-Timestamp']))) { + return true; + } + else { + debug('verify the x-gitee-token failed'); + } + } + } + return false; +} diff --git a/dist/provider/github/github-handler.d.ts b/dist/provider/github/github-handler.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bb3995face7a52c42ba30480d51b62a75654938 --- /dev/null +++ b/dist/provider/github/github-handler.d.ts @@ -0,0 +1,7 @@ +import { BaseTools, configOptionType } from '../../lib/base-tools'; +export declare class GiteeHandler extends BaseTools { + constructor(options: configOptionType); + handler(req: any, res: any, callback: any): any; + private verify; + private resSuccess; +} diff --git a/dist/provider/github/github-handler.js b/dist/provider/github/github-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..bd94e9537f13428debbad44fc8c6f1d203d6c917 --- /dev/null +++ b/dist/provider/github/github-handler.js @@ -0,0 +1,52 @@ +// src/provider/github/github-handler.ts +// V.2 ditch the external github-webhook-handler +import { BaseTools } from '../../lib/base-tools'; +import { verifyHandler } from './verify'; +import { debugFn } from '../../lib/helpers'; +const debug = debugFn('git-webhook-ci:github:handler'); +export class GiteeHandler extends BaseTools { + constructor(options) { + super(options); + } + // How to make this into the parent method + handler(req, res, callback) { + debug(`github handler called`); + return super.handler(req, res, this.verify) + .then(result => { + this.resSuccess(req, res, result); + }) + .catch(err => { + return callback(err); + }); + } + // github token verify method + verify(obj, opt) { + return new Promise((resolver, rejecter) => { + const { header, payload } = obj; + if (verifyHandler(header, opt.secret, payload)) { + resolver(payload); + } + else { + rejecter(new Error('Github verify failed')); + } + }); + } + resSuccess(req, res, result) { + res.writeHead(200, { 'content-type': 'application/json' }); + // this might be different take a look at the github module + res.end('{"ok":true}'); + if (result.hook_name === 'push_hooks') { // @TODO check if this is still correct + this.emit('push', { + payload: result, + host: req.headers.host, + event: result.hook_name + }); + } + else { + this.emit('error', { + msg: 'Not the event we are expecting', + event: result.hook_name + }); + } + } +} diff --git a/dist/provider/github/index.d.ts b/dist/provider/github/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3103deb326ade97e571da41ba9aac3b6dbc0684 --- /dev/null +++ b/dist/provider/github/index.d.ts @@ -0,0 +1,3 @@ +import { configOptionType } from '../../lib'; +declare function createGithubServer(config: configOptionType, callback: any, errorHandler?: any): any; +export default createGithubServer; diff --git a/dist/provider/github/index.js b/dist/provider/github/index.js new file mode 100644 index 0000000000000000000000000000000000000000..57c9a30d783218d04c963bdf26589701845f5fb0 --- /dev/null +++ b/dist/provider/github/index.js @@ -0,0 +1,37 @@ +// src/provider/github/index.ts +// Github is using another external npm to handle it +import { createServer, debugFn } from '../../lib'; +import githubWebhook from 'github-webhook-handler'; +const debug = debugFn('git-webhook-ci:github'); +// main method +function createGithubServer(config, callback, errorHandler = () => { }) { + const handler = githubWebhook({ + path: config.path, + secret: config.secret + }); + handler.on('error', err => { + errorHandler(err); + debug('Error:', err.message); + }); + // On received push event + handler.on('push', result => { + const ref = result.payload.ref; + if (config.branch === '*' || config.branch === ref) { + callback(result, config, ref); + } + else { + const errorStr = `Received a push event for ${result.payload.repository.name} to ${ref}`; + debug(errorStr); + errorHandler(errorStr); + } + }); + return createServer((req, res) => { + handler(req, res, (err) => { + debug(`The url got called! ${req.url}`, err); + errorHandler(err); + res.statusCode = 404; + res.end('-- no such location --'); + }); + }, config, debug); +} +export default createGithubServer; diff --git a/dist/provider/github/verify.d.ts b/dist/provider/github/verify.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1f92dd4a5694549459b4edce11156127dfaab51 --- /dev/null +++ b/dist/provider/github/verify.d.ts @@ -0,0 +1,14 @@ +/** + * Github now change the way how to validate the signature + * We need to implement it here, and it's using the payload to hash + * the whole key starts with `sha256=` using a HMAC hex + */ +export declare function getToken(secretKey: string, payload: any): string; +/** + * This is the actual call inside the verify method in the class + * @param {object} header the raw header + * @param {string} secretKey + * @param {object} payload the full payload encoded in utf8 + * @return {boolean} + */ +export declare function verifyHandler(header: any, secretKey: string, payload: any): boolean; diff --git a/dist/provider/github/verify.js b/dist/provider/github/verify.js new file mode 100644 index 0000000000000000000000000000000000000000..507e854e52cafa0ad65c9c83a17ac9d43f9ba8a5 --- /dev/null +++ b/dist/provider/github/verify.js @@ -0,0 +1,25 @@ +// src/provider/github/verify.ts +import { createHmac } from 'crypto'; +/** + * Github now change the way how to validate the signature + * We need to implement it here, and it's using the payload to hash + * the whole key starts with `sha256=` using a HMAC hex + */ +export function getToken(secretKey, payload) { + const hmac = createHmac('sha256', secretKey); + hmac.update(payload); // note here, you must make sure this is utf-8 encoded before passing here + return 'sha256=' + hmac.digest('hex'); // return the hex format +} +/** + * This is the actual call inside the verify method in the class + * @param {object} header the raw header + * @param {string} secretKey + * @param {object} payload the full payload encoded in utf8 + * @return {boolean} + */ +export function verifyHandler(header, secretKey, payload) { + if (header['X-Hub-Signature-256']) { + return header['X-Hub-Signature-256'] === getToken(secretKey, payload); + } + return false; +} diff --git a/dist/provider/gitlab/gitlab-handler.d.ts b/dist/provider/gitlab/gitlab-handler.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..36f5aa6c7fa52e83e65a0f6ec40c0f920db67e59 --- /dev/null +++ b/dist/provider/gitlab/gitlab-handler.d.ts @@ -0,0 +1,26 @@ +import { BaseTools, configOptionType } from '../../lib/base-tools'; +export declare class GitlabHandler extends BaseTools { + constructor(options: configOptionType); + /** + * Main method + * @param {object} req the request + * @param {object} res the respond + * @param {function} callback res with 404 + * @param {function} errorHandler optional error callback + * @return {null} nothing + */ + handler(req: any, res: any, callback: any): any; + /** + * Verify the password field + * @param {object} payload Content + * @param {object} headers headers looking for the X-Gitlab-Event: Push Hook + * @return {object} promise + */ + private verify; + /** + * @param {object} res the respond + * @param {object} result the payload + * @return {null} nothing + */ + private resSuccess; +} diff --git a/dist/provider/gitlab/gitlab-handler.js b/dist/provider/gitlab/gitlab-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..9211c5c829c1b86294df7486c1a8ed2b823c371a --- /dev/null +++ b/dist/provider/gitlab/gitlab-handler.js @@ -0,0 +1,68 @@ +// src/provider/gitlab/gitlab-handler.ts +import { BaseTools } from '../../lib/base-tools'; +import { debugFn } from '../../lib'; +const debug = debugFn('git-webhook-ci:gitlab'); +export class GitlabHandler extends BaseTools { + constructor(options) { + super(options); + } + /** + * Main method + * @param {object} req the request + * @param {object} res the respond + * @param {function} callback res with 404 + * @param {function} errorHandler optional error callback + * @return {null} nothing + */ + handler(req, res, callback) { + return super.handler(req, res, this.verify) + .then(result => { + this.resSuccess(req, res, result); + }) + .catch(callback); + } + /** + * Verify the password field + * @param {object} payload Content + * @param {object} headers headers looking for the X-Gitlab-Event: Push Hook + * @return {object} promise + */ + verify(obj, opt) { + const eventName = 'X-Gitlab-Event'; + const token = 'X-Gitlab-Token'; + const { header, payload } = obj; + // Console.log('headers', headers, typeof headers); + return new Promise((resolver, rejecter) => { + if (header[eventName] === 'Push Hook' && header[token] === opt.secret) { + resolver(payload); + } + else { + debug(header); + rejecter(new Error('Gitlab verify failed')); + } + }); + } + /** + * @param {object} res the respond + * @param {object} result the payload + * @return {null} nothing + */ + resSuccess(req, res, payload) { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end('{"ok":true}'); + // Check the result if this is what we wanted + if (payload.object_kind === 'push') { + this.emit('push', { + payload: payload, + host: req.headers.host, + event: payload.object_kind + }); + } + else { + this.emit('error', { + msg: 'Not the event we are expecting', + event: payload.object_kind + }); + } + } +} diff --git a/dist/provider/gitlab/index.d.ts b/dist/provider/gitlab/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..227d8e440f4beef71a6a85bd774cd300d64f6a16 --- /dev/null +++ b/dist/provider/gitlab/index.d.ts @@ -0,0 +1,3 @@ +import { configOptionType } from '../../lib'; +declare function createGitlabServer(config: configOptionType, callback: any, errorHandler?: any): any; +export default createGitlabServer; diff --git a/dist/provider/gitlab/index.js b/dist/provider/gitlab/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a5ab7f5faaa8e0b3abd1dd71410fd06ee010fc6e --- /dev/null +++ b/dist/provider/gitlab/index.js @@ -0,0 +1,34 @@ +// src/provider/gitlab/index.ts +import { GitlabHandler } from './gitlab-handler'; +import { createServer, debugFn } from '../../lib'; +const debug = debugFn('git-webhook-ci:gitlab'); +// main method +function createGitlabServer(config, callback, errorHandler = () => { }) { + const gitlab = new GitlabHandler(config); + gitlab.on('error', (err) => { + debug('error', err); + errorHandler(err); + }); + // Listen on the push event - success + gitlab.on('push', (result) => { + const ref = result.payload.ref; + if (config.branch === '*' || config.branch === ref) { + debug(`Gitlab Call success`); + callback(result, config, ref); + } + else { + errorHandler(ref); + debug('Gitee webhook is not expecting this branch', ref); + } + }); + // return the http server + return createServer((req, res) => { + gitlab.handler(req, res, (err) => { + debug('The url got called! [%s]', req.url, err); + errorHandler(err); + res.statusCode = 404; + res.end('-- no such location --'); + }); + }, config, debug); +} +export default createGitlabServer; diff --git a/dist/provider/index.d.ts b/dist/provider/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d4043066f21646887e99eabcaea82832a47c084 --- /dev/null +++ b/dist/provider/index.d.ts @@ -0,0 +1,7 @@ +/** + * Wrap all the configuration check code here + * Then init the instance and return it + * @NOTE v.2 gitee now is the default + * also it's now a named export + */ +export declare function getProvider(provider: string): any; diff --git a/dist/provider/index.js b/dist/provider/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c84d52600c9d1db8c63f55bb8de9cfeffc2c172d --- /dev/null +++ b/dist/provider/index.js @@ -0,0 +1,26 @@ +// src/provider/index.ts +// group everything together and call from here +import giteeWebhook from './gitee'; +import githubWebhook from './github'; +import gitlabWebhook from './gitlab'; +import wechatWebhook from './wechat'; +/** + * Wrap all the configuration check code here + * Then init the instance and return it + * @NOTE v.2 gitee now is the default + * also it's now a named export + */ +export function getProvider(provider) { + switch (provider) { + case 'wechat': + return wechatWebhook; + case 'gitlab': + return gitlabWebhook; + case 'github': + return githubWebhook; + case 'gitee': + return giteeWebhook; + default: + throw new Error(`Unknown provider`); + } +} diff --git a/dist/provider/wechat/index.d.ts b/dist/provider/wechat/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..c37373c658cb5af92d139c9dff91152ef1242dee --- /dev/null +++ b/dist/provider/wechat/index.d.ts @@ -0,0 +1,3 @@ +import { configOptionType } from '../../lib'; +declare function createWechatServer(config: configOptionType, opt: any, callback: any, errorHandler?: any): any; +export default createWechatServer; diff --git a/dist/provider/wechat/index.js b/dist/provider/wechat/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e3dcbe30cd5ddb5438bea3977a5579b2d33fb0d9 --- /dev/null +++ b/dist/provider/wechat/index.js @@ -0,0 +1,24 @@ +// src/provider/wechat/index.ts +import { WechatHandler } from './wechat-handler'; +import { createServer, debugFn } from '../../lib'; +const debug = debugFn('git-webhook-ci:wechat'); +function createWechatServer(config, opt, callback, errorHandler = () => { }) { + const wechat = new WechatHandler(config); + /* This is not implemented, there is no need at the moment + wechat.on('error', err => { + debug('error', err); + }); + */ + wechat.on('push', result => { + callback(result, opt); + }); + return createServer((req, res) => { + wechat.handler(req, res, (err) => { + errorHandler(err); + debug('The url got called! [%s]', req.url, err); + res.statusCode = 404; + res.end('-- no such location --'); + }); + }, config, debug); +} +export default createWechatServer; diff --git a/dist/provider/wechat/wechat-handler.d.ts b/dist/provider/wechat/wechat-handler.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..2910ed556b4954231333a458c4a1319298eede60 --- /dev/null +++ b/dist/provider/wechat/wechat-handler.d.ts @@ -0,0 +1,22 @@ +/** + * This is for wechat mini-app push callback + * Based on their PHP version + */ +import { BaseTools, configOptionType } from '../../lib'; +export declare class WechatHandler extends BaseTools { + constructor(options: configOptionType); + /** + * Main interface, this is different from the other because there is no filter + * on what is coming, just verify it then pass the payload to the callback + */ + handler(req: any, res: any, callback: any): any; + private getParams; + /** + * This is different using the query parameter to compare + * Another thing is - this is a one off verify process + * once the wechat end verify this end is correct, it will + * just send data over. Need to figure out a way to run this + * verify before the actual listening + */ + private verify; +} diff --git a/dist/provider/wechat/wechat-handler.js b/dist/provider/wechat/wechat-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..3b0690456b9b8f018c585a7adec93c9106944a52 --- /dev/null +++ b/dist/provider/wechat/wechat-handler.js @@ -0,0 +1,86 @@ +/** + * This is for wechat mini-app push callback + * Based on their PHP version + */ +/* +https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/callback_help.html + +private function checkSignature() +{ + $signature = $_GET["signature"]; + $timestamp = $_GET["timestamp"]; + $nonce = $_GET["nonce"]; + + $token = TOKEN; + $tmpArr = array($token, $timestamp, $nonce); + sort($tmpArr, SORT_STRING); + $tmpStr = implode( $tmpArr ); + $tmpStr = sha1( $tmpStr ); + + if( $tmpStr == $signature ){ + return true; + }else{ + return false; + } +} +*/ +import { BaseTools, debugFn } from '../../lib'; +import { URL } from 'url'; +import sha1 from 'sha1'; +const debug = debugFn('git-webhook-ci:wechat'); +export class WechatHandler extends BaseTools { + constructor(options) { + super(options); + } + /** + * Main interface, this is different from the other because there is no filter + * on what is coming, just verify it then pass the payload to the callback + */ + handler(req, res, callback) { + if (this.options.inited !== true) { + const echostr = this.verify(req); + if (!echostr) { + debug('verify with wechat server failed'); + return callback('verify failed'); + } + debug(`verify with wechat echostr '${echostr}' correct`); + res.writeHead(200, { 'content-type': 'text/html' }); + res.end(echostr); + return; + } + // The implementation is different + this.parsePayload(req) + .then(payload => { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end('{"ok": true}'); + // Just reuse the same naming + this.emit('push', { + payload, + host: req.headers.host, + event: 'wechat-push' + }); + }); + } + // get the params from url + getParams(url) { + const u = new URL(url); + const params = ['signature', 'timestamp', 'nonce', 'echostr']; + return params.map((param) => { + return { [param]: u.searchParams.get(param) }; + }).reduce((a, b) => Object.assign(a, b), {}); + } + /** + * This is different using the query parameter to compare + * Another thing is - this is a one off verify process + * once the wechat end verify this end is correct, it will + * just send data over. Need to figure out a way to run this + * verify before the actual listening + */ + verify(req) { + const { signature, timestamp, nonce, echostr } = this.getParams(req.url); + const $token = this.options.secret; + let $tmpArr = [$token, timestamp, nonce]; + $tmpArr.sort(); + return sha1($tmpArr.join('')) === signature ? echostr : false; + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000000000000000000000000000000000000..98508704e8e596bc35cde029436b3cb4096d5f50 --- /dev/null +++ b/index.js @@ -0,0 +1,5 @@ +// index.js the main entry file + +const { gitWebhookCi } = require('./dist/main') + +module.exports = gitWebhookCi \ No newline at end of file diff --git a/package.json b/package.json index 45b192c2b43a06a4c878fda549b335595e929056..795acbc5292bc2011b83bfd955788cc0e609b298 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { "name": "git-webhook-ci", - "version": "2.0.0-alpha.1", + "version": "2.0.0-beta", "description": "Using git provider webhook to create a poorman CI system ", - "main": "dist/index.js", + "main": "index.js", "files": [ - "docs", - "src", + "dist", "clean.js", - "run.js" + "index.js" ], "scripts": { "test": "ava", diff --git a/run.ts b/run.ts index d7952a76a2dd73f4f293430ccc93ad368b5a4047..4b65bd26ea5f92c9a67a9839ef5c3087ae113ec3 100644 --- a/run.ts +++ b/run.ts @@ -18,4 +18,4 @@ const config = { } } -gitWebhookCi(config) +gitWebhookCi(config) \ No newline at end of file diff --git a/src/provider/wechat/wechat-handler.ts b/src/provider/wechat/wechat-handler.ts index af2d29a26768c095a3832912b2810b3aac47ad37..6e5d4bfa54851f4580774fa157efe4a0de4b22c3 100644 --- a/src/provider/wechat/wechat-handler.ts +++ b/src/provider/wechat/wechat-handler.ts @@ -39,7 +39,7 @@ export class WechatHandler extends BaseTools { * Main interface, this is different from the other because there is no filter * on what is coming, just verify it then pass the payload to the callback */ - public handler(req: any, res: any, callback: any): void { + public handler(req: any, res: any, callback: any) { if (this.options.inited !== true) { const echostr = this.verify(req) if (!echostr) { diff --git a/tests/fixtures/github.ts b/tests/fixtures/github.ts index 52f4e311615e10df7b599b8ea25e0d3a6525e3d8..15b302e54914ae8fc17ba9980bc581d3fe739cd9 100644 --- a/tests/fixtures/github.ts +++ b/tests/fixtures/github.ts @@ -5,5 +5,151 @@ export const header = { } export const payload = { - + "zen": "Mind your words, they are important.", + "hook_id": 304189869, + "hook": { + "type": "Repository", + "id": 304189869, + "name": "web", + "active": true, + "events": [ + "push" + ], + "config": { + "content_type": "json", + "insecure_ssl": "0", + "secret": "********", + "url": "https://81c940876e26.ngrok.io" + }, + "updated_at": "2021-06-24T13:42:48Z", + "created_at": "2021-06-24T13:42:48Z", + "url": "https://api.github.com/repos/to1source/git-webhook-ci/hooks/304189869", + "test_url": "https://api.github.com/repos/to1source/git-webhook-ci/hooks/304189869/test", + "ping_url": "https://api.github.com/repos/to1source/git-webhook-ci/hooks/304189869/pings", + "last_response": { + "code": null, + "status": "unused", + "message": null + } + }, + "repository": { + "id": 379456101, + "node_id": "MDEwOlJlcG9zaXRvcnkzNzk0NTYxMDE=", + "name": "git-webhook-ci", + "full_name": "to1source/git-webhook-ci", + "private": false, + "owner": { + "login": "to1source", + "id": 31663067, + "node_id": "MDQ6VXNlcjMxNjYzMDY3", + "avatar_url": "https://avatars.githubusercontent.com/u/31663067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/to1source", + "html_url": "https://github.com/to1source", + "followers_url": "https://api.github.com/users/to1source/followers", + "following_url": "https://api.github.com/users/to1source/following{/other_user}", + "gists_url": "https://api.github.com/users/to1source/gists{/gist_id}", + "starred_url": "https://api.github.com/users/to1source/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/to1source/subscriptions", + "organizations_url": "https://api.github.com/users/to1source/orgs", + "repos_url": "https://api.github.com/users/to1source/repos", + "events_url": "https://api.github.com/users/to1source/events{/privacy}", + "received_events_url": "https://api.github.com/users/to1source/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/to1source/git-webhook-ci", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/to1source/git-webhook-ci", + "forks_url": "https://api.github.com/repos/to1source/git-webhook-ci/forks", + "keys_url": "https://api.github.com/repos/to1source/git-webhook-ci/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/to1source/git-webhook-ci/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/to1source/git-webhook-ci/teams", + "hooks_url": "https://api.github.com/repos/to1source/git-webhook-ci/hooks", + "issue_events_url": "https://api.github.com/repos/to1source/git-webhook-ci/issues/events{/number}", + "events_url": "https://api.github.com/repos/to1source/git-webhook-ci/events", + "assignees_url": "https://api.github.com/repos/to1source/git-webhook-ci/assignees{/user}", + "branches_url": "https://api.github.com/repos/to1source/git-webhook-ci/branches{/branch}", + "tags_url": "https://api.github.com/repos/to1source/git-webhook-ci/tags", + "blobs_url": "https://api.github.com/repos/to1source/git-webhook-ci/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/to1source/git-webhook-ci/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/to1source/git-webhook-ci/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/to1source/git-webhook-ci/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/to1source/git-webhook-ci/statuses/{sha}", + "languages_url": "https://api.github.com/repos/to1source/git-webhook-ci/languages", + "stargazers_url": "https://api.github.com/repos/to1source/git-webhook-ci/stargazers", + "contributors_url": "https://api.github.com/repos/to1source/git-webhook-ci/contributors", + "subscribers_url": "https://api.github.com/repos/to1source/git-webhook-ci/subscribers", + "subscription_url": "https://api.github.com/repos/to1source/git-webhook-ci/subscription", + "commits_url": "https://api.github.com/repos/to1source/git-webhook-ci/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/to1source/git-webhook-ci/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/to1source/git-webhook-ci/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/to1source/git-webhook-ci/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/to1source/git-webhook-ci/contents/{+path}", + "compare_url": "https://api.github.com/repos/to1source/git-webhook-ci/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/to1source/git-webhook-ci/merges", + "archive_url": "https://api.github.com/repos/to1source/git-webhook-ci/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/to1source/git-webhook-ci/downloads", + "issues_url": "https://api.github.com/repos/to1source/git-webhook-ci/issues{/number}", + "pulls_url": "https://api.github.com/repos/to1source/git-webhook-ci/pulls{/number}", + "milestones_url": "https://api.github.com/repos/to1source/git-webhook-ci/milestones{/number}", + "notifications_url": "https://api.github.com/repos/to1source/git-webhook-ci/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/to1source/git-webhook-ci/labels{/name}", + "releases_url": "https://api.github.com/repos/to1source/git-webhook-ci/releases{/id}", + "deployments_url": "https://api.github.com/repos/to1source/git-webhook-ci/deployments", + "created_at": "2021-06-23T02:27:15Z", + "updated_at": "2021-06-23T13:39:20Z", + "pushed_at": "2021-06-23T13:39:15Z", + "git_url": "git://github.com/to1source/git-webhook-ci.git", + "ssh_url": "git@github.com:to1source/git-webhook-ci.git", + "clone_url": "https://github.com/to1source/git-webhook-ci.git", + "svn_url": "https://github.com/to1source/git-webhook-ci", + "homepage": null, + "size": 162, + "stargazers_count": 0, + "watchers_count": 0, + "language": "TypeScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "wtfpl", + "name": "Do What The F*ck You Want To Public License", + "spdx_id": "WTFPL", + "url": "https://api.github.com/licenses/wtfpl", + "node_id": "MDc6TGljZW5zZTE4" + }, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "to1source", + "id": 31663067, + "node_id": "MDQ6VXNlcjMxNjYzMDY3", + "avatar_url": "https://avatars.githubusercontent.com/u/31663067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/to1source", + "html_url": "https://github.com/to1source", + "followers_url": "https://api.github.com/users/to1source/followers", + "following_url": "https://api.github.com/users/to1source/following{/other_user}", + "gists_url": "https://api.github.com/users/to1source/gists{/gist_id}", + "starred_url": "https://api.github.com/users/to1source/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/to1source/subscriptions", + "organizations_url": "https://api.github.com/users/to1source/orgs", + "repos_url": "https://api.github.com/users/to1source/repos", + "events_url": "https://api.github.com/users/to1source/events{/privacy}", + "received_events_url": "https://api.github.com/users/to1source/received_events", + "type": "User", + "site_admin": false + } } diff --git a/tests/gitee.test.ts b/tests/gitee.test.ts index 867f1626f12423b5f16e08dcbde6a18e5f3096bf..62bf35a47acb895c5857862be16519543b9b305e 100644 --- a/tests/gitee.test.ts +++ b/tests/gitee.test.ts @@ -7,30 +7,29 @@ import { gitWebhookCi } from '../src/main' import { SECRET_KEY } from './fixtures/secret' import { getFakeData } from './fixtures/fake-callback' - test.cb(`Should able to use a gitee config to listen to the webhook event`, t => { t.plan(1) const { header, payload } = getFakeData('gitee') + const srv = gitWebhookCi({ + provider: 'gitee', + secret: SECRET_KEY, + cmd: (...args) => { + + console.log(args) - request( - gitWebhookCi({ - provider: 'gitee', - secret: SECRET_KEY, - cmd: (...args) => { + t.pass() + t.end() + } + }) - console.log(args) - t.pass() - t.end() - } + request(srv) + .post('/webhook') + .set(header) + .send(payload) + .expect(200, () => { // we must call here to let supertest to exeucte the call + // console.log(`200 back`) }) - ) - .post('/webhook') - .set(header) - .send(payload) - .expect(200, () => { // we must call here to let supertest to exeucte the call - // console.log(`200 back`) - }) }) diff --git a/tests/partial.test.ts b/tests/partial.test.ts index 20a73d5b27782fa50cc06feea4fddd907d01b4f8..92a17fb0494302b5a5c49775f04084c3ec6a50e7 100644 --- a/tests/partial.test.ts +++ b/tests/partial.test.ts @@ -5,7 +5,7 @@ import request from 'supertest' // and testing the code part by part import createServer from '../src/lib/server' -import { header, payload } from './fixtures/gitee.ts' +import { header, payload } from './fixtures/gitee' // @NOTE should sub class from the base-tools but this will do for now function parseHeader(req): any {