diff --git a/docs/api_reference.md b/docs/api_reference.md index 61aafae..84b8a81 100644 --- a/docs/api_reference.md +++ b/docs/api_reference.md @@ -72,6 +72,30 @@ console.log(response); - viu - the path to viu folder, if it doesnot work it will fall back to auto download - if it not works it throws an error +- gcloud + - This params can be `JSON String || Javascript Object || JSON File Buffer` + - Google Cloud Service Account Credentials having permission of `serviceusage.services.use` + - if this parameter is present we won't use viu for that process + - `Note: This service is chargeable by Google and follow their terms and conditions` + - To try this use below steps + 1. Create a Google Cloud account (Skip this step if you already have a Google Cloud account). + 2. Create a new, separate project. + 3. In the newly created project: + - Search for `IAM & Admin` in the Google Cloud Console. + - Go to `Roles` under IAM. + 4. Create a new role: + - Set the title, description, and ID of your choice. + - Set the Role Launch Stage to `General Availability`. + 5. Add the `serviceusage.services.use` permission to the role and click **Create**. + 6. Create a A New Service Account within IAM & Admin Page In `Service account details` - Give a Name, id, description of your choice and then in + - `Service account details` - Give a Name, id, description of your choice + - `Grant this service account access to project` - Attach the role that you created in previous step by searching your given name + - `Grant users access to this service account (optional)` - Leave Empty + - Then Click **Done** + 7. Go To service Accounts List of your project and Click on the email that you created in previous step download and Go to `Keys`and then `Create New Key - JSON`. You will get a json file in your browser downloads - `Keep this JSON` + 8. Search for `Cloud Vision API` in the Google Cloud Console and `Enable` it (Ignore, if its not already enabled) + + The example input is as follows @@ -81,6 +105,19 @@ const irctc = new IRCTC( "userID":"XXXXXX", "password":"XXXXXXXXX", "viu":"./some/loaction/to/file.exe | ./some/loaction/to/file" // Optional + "gcloud":{ + "type": "service_account", + "project_id": "vision-api", + "private_key_id": "b0357d061ce2c96737d96c2", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqdyztLFNG\n-----END PRIVATE KEY-----\n", + "client_email": "default@vision-api.iam.gserviceaccount.com", + "client_id": "12345678901234567", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/default%40vision-api.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" + } }); ``` @@ -89,6 +126,12 @@ const irctc = new IRCTC( `book` function in IRCTC class takes input as a javascript object, where they are explained below +``` +Note: + +for book_input function, there are a set of mandatory keys and a set of optional keys. Optional Keys means they're not compulsory to be passed. +``` + - `Mandatory Keys` - payment - for UPI payment @@ -174,8 +217,10 @@ const irctc = new IRCTC( - Must be a string and should match the with the list of existing station code names - Must be short code of the station from where you are boarding - The Train must pass by and have a stop at this station --gst + - board is the station where the passenger will be actually catching the train. this should not be confused with the mandatory from parameter which is a param for defining the starting point for the ticket. for eg. the passenger may book a train ticket from DEL to MUM, but he may prefer to join the journey at any intermediate station like AGC. +- gst - Must be a string and it must be the 17 digit GSTIN number + - This param is not necessary unless you are a business owner and want to claim the gst later. diff --git a/lib/index.mjs b/lib/index.mjs index e59424d..5e19add 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -10,13 +10,19 @@ import {viupath} from "./utils/viu.mjs" import {book_validator} from './utils/form_validator.mjs'; import {stations} from "./utils/stations.mjs"; import {countries} from "./utils/countries.mjs"; +import {vision_api} from "./utils/gcloud.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); -async function viu_captcha(params={}){ - const rl = readline.createInterface({ input, output }); - const answer = await rl.question(`${execFileSync(params.viu, [params.captcha_path,"-t"])} \nPlease type the above text and press enter\n`); - rl.close(); - return answer; +async function answer_captcha(params={},captcha){ + if(Object.prototype.hasOwnProperty.call(params, "gcloud")){ + return await vision_api(params,captcha); + }else{ + writeFileSync(params.captcha_path,Buffer.from(captcha, 'base64')); + const rl = readline.createInterface({ input, output }); + const answer = await rl.question(`${execFileSync(params.viu, [params.captcha_path,"-t"])} \nPlease type the above text and press enter\n`); + rl.close(); + return answer; + } } function normal_sleep(ms){ @@ -153,8 +159,7 @@ async function login(params={}){ options.headers.greq = params.csrf; const {body} = await params.browse.request("https://www.irctc.co.in/eticketing/protected/mapps1/loginCaptcha?nlpCaptchaException=true",options); params.status = body.status; - writeFileSync(params.captcha_path,Buffer.from(body.captchaQuestion, 'base64')); - const answer = await viu_captcha(params); + const answer = await answer_captcha(params,body.captchaQuestion); await custom_sleep({ "slot":params.slot, "callfrom":"login", @@ -427,8 +432,7 @@ async function confirm_booking_form(book_params={},params={}){ headersa["Authorization"] = params.access_token; headersa["bmiyek"] = params.user_hash; while (book_params["captchaDto"]["captchastatus"] !== "SUCCESS"){ - writeFileSync(params.captcha_path,Buffer.from(book_params["captchaDto"]["captchaQuestion"], 'base64')); - let answer = await viu_captcha(params); + let answer = await answer_captcha(params,book_params["captchaDto"]["captchaQuestion"]); headersa['spa-csrf-token'] = params.csrf; let response = await params.browse.request( `https://www.irctc.co.in/eticketing/protected/mapps1/captchaverify/${book_params.tid}/BOOKINGWS/${answer}`, diff --git a/lib/utils/gcloud.mjs b/lib/utils/gcloud.mjs new file mode 100644 index 0000000..0cb9771 --- /dev/null +++ b/lib/utils/gcloud.mjs @@ -0,0 +1,177 @@ +import { createSign } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { request } from "node:https"; +import { URLSearchParams } from "node:url"; + +async function checker(service_account,params){ + function isPlainObject(value) { + return ( + typeof value === 'object' && + value !== null && + !Buffer.isBuffer(value) && + !Array.isArray(value) && + value.constructor === Object + ); + } + + function isgcloudservice(value){ + return ( + isPlainObject(value) && + Object.prototype.hasOwnProperty.call(value,"type") && + typeof value.type === "string" && + value.type === "service_account" && + Object.prototype.hasOwnProperty.call(value,"private_key_id") && + Object.prototype.hasOwnProperty.call(value,"private_key") && + typeof value.private_key === "string" && + value.private_key.startsWith("-----BEGIN PRIVATE KEY-----") && + ( + value.private_key.endsWith("-----END PRIVATE KEY-----") || + value.private_key.endsWith("-----END PRIVATE KEY-----\n") + ) && + Object.prototype.hasOwnProperty.call(value,"client_email") && + typeof value.client_email === "string" && + value.client_email.endsWith(".iam.gserviceaccount.com") && + !Object.prototype.hasOwnProperty.call(value,"token") + ); + } + + if (typeof service_account === "string"){ + service_account = service_account.trim(); + if (service_account.startsWith('{') && service_account.endsWith('}')){ + service_account = JSON.parse(service_account); + return await checker(service_account,params); + }else{ + throw new Error(`Invalid Parameter: value for gcloud key must be an object or valid JSON file content provided by google`); + } + } else if (typeof service_account === "object" && Buffer.isBuffer(service_account)){ + return await checker(service_account.toString(),params); + } else if (isgcloudservice(service_account)){ + params.gcloud_project = service_account.project_id; + return await generate_token(service_account); + } else if (isPlainObject(service_account) && Object.prototype.hasOwnProperty.call(service_account,"token") && Object.prototype.hasOwnProperty.call(service_account,"project_id")){ + params.gcloud_project = service_account.project_id; + return service_account.token; + }else{ + throw new Error(`Invalid Parameter: value for gcloud key must be an object or valid JSON file content provided by google`); + } +} + +async function generate_token(service_account) { + try { + const header = Buffer.from( + JSON.stringify({ + alg: "RS256", + typ: "JWT", + kid: service_account.private_key_id, + }) + ).toString("base64url"); + const iat = Math.floor(Date.now() / 1000); + const payload = Buffer.from( + JSON.stringify({ + iss: service_account.client_email, + scope: "https://www.googleapis.com/auth/cloud-vision", + aud: "https://oauth2.googleapis.com/token", + exp: iat + 3600, + iat: iat, + }) + ).toString("base64url"); + const signature = createSign("RSA-SHA256").update(`${header}.${payload}`).sign(service_account.private_key, "base64url"); + const post_body = new URLSearchParams({ + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: `${header}.${payload}.${signature}` + }); + return new Promise((resolve, reject) => { + const req = request( + "https://oauth2.googleapis.com/token", + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }, + (res) => { + const chunks = []; + res.on("data", (chunk) => { + chunks.push(chunk); + }); + + res.on("end", () => { + const data = JSON.parse(Buffer.concat(chunks).toString()); + if (Object.prototype.hasOwnProperty.call(data,"access_token") && typeof data.access_token === "string" && data.access_token.length > 10){ + resolve(data.access_token); + } + else{ + reject(data); + } + }); + } + ); + req.on("error", (e) => { + reject(e); + }); + req.write(post_body.toString()); + req.end(); + }); + } catch (e) { + throw new Error(`Error generating token: ${e.message}`); + } +} + +async function vision_api(params={},captcha){ + try{ + if (!Object.prototype.hasOwnProperty.call(params,"gcloud_token")){ + params.gcloud_token = await checker(params.gcloud,params); + return await vision_api(params,captcha); + }else{ + return new Promise((resolve, reject) => { + const req = request("https://vision.googleapis.com/v1/images:annotate",{ + "method":"POST", + "headers": + { + "Authorization":`Bearer ${params.gcloud_token}`, + "x-goog-user-project":params.gcloud_project, + "Content-Type":"application/json; charset=utf-8" + } + },(res) =>{ + const chunks = []; + res.on("data", (chunk) => { + chunks.push(chunk); + }); + res.on("end", () => { + const data = Buffer.concat(chunks).toString(); + if (res.statusCode !== 200){ + reject(data); + }else{ + resolve((JSON.parse(data)).responses[0].fullTextAnnotation.text.replace(/[\s\n\r]/g,'')); + } + }); + }); + req.on("error", (e) => { + console.error("Request error:", e); + reject(e); + }); + req.write(JSON.stringify({ + "requests": [ + { + "image": { + "content": captcha + }, + "features": [ + { + "type": "TEXT_DETECTION" + } + ] + } + ] + } + )); + req.end(); + }); + } + } catch(e){ + throw new Error(`Error at Google Cloud Vision API:\n${e}`); + } +} + +export default vision_api; +export {vision_api}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5f425ea..ab8aa9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "irctc-api", - "version": "3.0.4", + "version": "3.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "irctc-api", - "version": "3.0.4", + "version": "3.0.5", "license": "Apache-2.0", "dependencies": { "tough-cookie": "^5.0.0", diff --git a/package.json b/package.json index 554f3fc..cdc00d3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "irctc-api", "description": "An exclusive NodeJs only package built on top of IRCTC Website APIs to book train tickets, managing user profile faster and simpler from anywhere in the world", - "version": "3.0.4", + "version": "3.0.5", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" },