Skip to content

Instantly share code, notes, and snippets.

@kritollm
Last active July 30, 2024 13:15
Show Gist options
  • Save kritollm/d263c4ffd8f7ae3e0b0ff64c3d370389 to your computer and use it in GitHub Desktop.
Save kritollm/d263c4ffd8f7ae3e0b0ff64c3d370389 to your computer and use it in GitHub Desktop.
Simple digestauth with got
import * as crypto from 'crypto';
import { OptionsInit, got } from 'got-scraping';
export interface GotDigestAuthOpts {
password: string;
username: string;
}
export default class GotDigestAuth {
private count: number;
private readonly password: string;
private readonly username: string;
constructor({ password, username }: GotDigestAuthOpts) {
this.count = 0;
this.password = password;
this.username = username;
}
private computeHa1(username: string, realm: string, password: string, nonce: string, cnonce: string, algorithm: string): string {
const baseHa1 = crypto.createHash('md5').update(`${username}:${realm}:${password}`).digest('hex');
if (algorithm.toLowerCase() === 'md5-sess') {
return crypto.createHash('md5').update(`${baseHa1}:${nonce}:${cnonce}`).digest('hex');
}
return baseHa1;
}
public async request(opts: OptionsInit) {
let shouldRetry = true;
let lastError: any = null;
do {
try {
const reqOpts = {
url: opts.url,
method: opts.method || 'GET',
headers: {
...(opts.headers || {}),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'nb-NO,nb;q=0.9,no;q=0.8,nn;q=0.7,en-US;q=0.6,en;q=0.5',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
'sec-ch-ua': '"Google Chrome";v="111", "Not(A:Brand";v="8", "Chromium";v="111"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': 'Windows'
}
};
const response = await got(reqOpts);
shouldRetry = false;
return response;
} catch (error: any) {
if (error.response === undefined || error.response.statusCode !== 401 || !error.response.headers['www-authenticate']?.includes('nonce')) {
throw error;
}
const authDetails = error.response.headers['www-authenticate'].split(',').map((v: string) => v.split('='));
const stale = authDetails.find((el: any) => el[0].toLowerCase().indexOf('stale') > -1);
if (this.count > 2) {
throw new Error('Maksimalt antall forsøk nådd, sjekk brukernavn eller passord');
}
if (stale && stale[1].toLowerCase() === 'true') {
// If stale=true, retry the request
this.count = 0;
return await this.request({
url: opts.url,
method: opts.method
});
}
++this.count;
const nonceCount = ('00000000' + this.count).slice(-8);
const cnonce = crypto.randomBytes(8).toString('hex');
const realm = authDetails.find((el: any) => el[0].toLowerCase().indexOf('realm') > -1)[1].replace(/"/g, '');
const nonce = authDetails.find((el: any) => el[0].toLowerCase().indexOf('nonce') > -1)[1].replace(/"/g, '');
const qopValue = authDetails.find((el: any) => el[0].toLowerCase().indexOf('qop') > -1)[1].replace(/"/g, '');
const algorithmValue = authDetails.find((el: any) => el[0].toLowerCase().indexOf('algorithm') > -1)[1].replace(/"/g, '');
//const charsetValue = authDetails.find((el: any) => el[0].toLowerCase().indexOf('charset') > -1)[1].replace(/"/g, '');
const ha1 = this.computeHa1(this.username, realm, this.password, nonce, cnonce, algorithmValue);
const parsedUrl = new URL(typeof opts.url === 'string' ? opts.url : (opts.url as URL).href);
const path = parsedUrl.pathname + (parsedUrl.search || '');
const ha2 = crypto.createHash('md5').update(`${opts.method ?? 'GET'}:${path}`).digest('hex');
const response = crypto.createHash('md5').update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`).digest('hex');
const authorization = `Digest username="${this.username}",realm="${realm}",` +
`nonce="${nonce}",uri="${path}",qop="${qopValue}",algorithm="${algorithmValue}",` +
`response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`; //,` +
//`charset="${charsetValue}"`;
if (opts.headers) {
opts.headers['Authorization'] = authorization;
} else {
opts.headers = { Authorization: authorization };
}
lastError = error;
}
} while (shouldRetry);
throw lastError;
}
}
// Example
const url = 'https://api.domain.no/service/service.asmx';
const APIKey = 'xxx-xxx-xxxx-xxx-xxxxxx';
const username = 'your-user-name';
const password = 'your-password'
const digestAuth = new GotDigestAuth({
username,
password
});
async function getPriceList() {
const resp = await digestAuth.request({
url: `${url}/GetPriceList?APIKey=${APIKey}`,
}).catch(e => null);
if (!resp) {
return null;
}
await writeFile('getpricelist.xml', resp.body, 'utf8').catch(() => null);
const json = await convertXmlToJson(resp.body);
if (!json) {
return null;
}
await prettyJSON('getpricelist.json', json);
return json;
}
import { parseStringPromise, processors } from 'xml2js';
async function convertXmlToJson(xml: string): Promise<any> {
try {
const options = {
mergeAttrs: true,
trim: true,
normalizeTags: false,
explicitArray: false,
ignoreAttrs: false,
tagNameProcessors: [processors.firstCharLowerCase],
attrNameProcessors: [processors.firstCharLowerCase],
valueProcessors: [
(value: string, name: string) => {
if (typeof value === 'string') {
return value.replace(/\u00a0/g, ' ');
}
return value;
},
],
};
const json = await parseStringPromise(xml, options);
return json;
} catch (error) {
console.error('Error converting XML to JSON:', error);
}
}
import { readFile, writeFile } from 'node:fs/promises';
import { logger } from './logger.js';
async function prettyJSON(filename, data?) {
if (data) {
await writeFile(filename, JSON.stringify(data, null, 2)).catch(e => {
logger.error(e);
});
} else {
await writeFile(filename, JSON.stringify(JSON.parse(await readFile(filename, 'utf8')), null, 2));
}
}
import { createLogger, format, transports } from "winston";
const logger = createLogger({
level: 'info',
format: format.json(),
defaultMeta: { service: 'Lager oppdatering' },
transports: [
//
// - Write all logs with level `error` and below to `error.log`
// - Write all logs with level `info` and below to `combined.log`
//
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' }),
],
});
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
logger.add(new transports.Console({
format: format.simple(),
}));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment