Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions examples/login-with-custom-http-library.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* This example demonstrates how to use a custom HTTP library with steam-session
* instead of the default @doctormckay/stdlib HttpClient.
*
* This allows you to use popular HTTP libraries like got, axios, node-fetch, or request.
*/

import {
LoginSession,
EAuthTokenPlatformType,
CustomRequestFunction
} from '../src';

import got from 'got';
import { SocksProxyAgent } from 'socks-proxy-agent';


async function exampleWithProxy() {
const sessionId = Math.random().toString(36).substring(2, 10); // Generate a random session ID for proxy authentication
// Using proxy - comment out if you don't have valid proxy credentials
const proxyAgent = new SocksProxyAgent(`socks5://xxxx-${sessionId}:xxx@proxy.xxxx.xxx:3120`);

const customRequest: CustomRequestFunction = async (options) => {
try {
// Prepare got options
const gotOptions: any = {
method: options.method,
searchParams: options.queryParams,
agent: { // Comment out if not using proxy
https: proxyAgent
},
followRedirect: true,
responseType: 'buffer', // Use buffer to handle both text and binary responses
timeout: {
request: 10000 // 10 second timeout
},
headers: options.headers
};

// Handle body if provided (already encoded, e.g. as multipart/form-data)
if (options.body) {
gotOptions.body = options.body;
}

console.log('Making request to:', options.url);
console.log('Method:', options.method);
console.log('Headers:', gotOptions.headers);
console.log('Body type:', options.body ? (Buffer.isBuffer(options.body) ? 'Buffer' : 'string') : 'none');

const response = await got(options.url, gotOptions);

return {
statusCode: response.statusCode,
headers: response.headers,
body: response.body, // This will be a Buffer
finalUrl: response.url
};
} catch (error: any) {
console.error('Request failed:', error.message);
if (error.response) {
console.error('Response status:', error.response.statusCode);
console.error('Response body:', error.response.body?.toString());
return {
statusCode: error.response.statusCode,
headers: error.response.headers,
body: error.response.body,
finalUrl: error.response.url
};
}
throw error;
}
};

let session = new LoginSession(EAuthTokenPlatformType.WebBrowser, {
customRequestFunction: customRequest
});

session.on('authenticated', async () => {
console.log('Successfully authenticated!');

// Get web cookies
let cookies = await session.getWebCookies();
console.log('Got cookies:', cookies);
});

session.on('timeout', () => {
console.log('Login timed out');
});

session.on('error', (err) => {
console.error('Login error:', err);
});

// Start login
let startResult = await session.startWithCredentials({
accountName: 'xxx',
password: 'xxxxx'
});

console.log('Login started:', startResult);

}

exampleWithProxy();
171 changes: 171 additions & 0 deletions src/HttpClientAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {CustomRequestFunction, CustomRequestOptions, CustomRequestResponse} from './interfaces-external';

/**
* Adapter that wraps a custom request function to match the HttpClient interface
* expected by the rest of the library.
*/
export default class HttpClientAdapter {
private _customRequestFunction: CustomRequestFunction;
private _userAgent?: string;

constructor(customRequestFunction: CustomRequestFunction) {
this._customRequestFunction = customRequestFunction;
}

/**
* User-agent string to include in requests
*/
get userAgent(): string | undefined {
return this._userAgent;
}

set userAgent(value: string) {
this._userAgent = value;
}

/**
* Static method to convert a plain object to multipart form data.
* This is called by HttpClient.simpleObjectToMultipartForm() in the existing code.
*
* For the adapter, we'll return the object directly and handle conversion in the request method.
*/
static simpleObjectToMultipartForm(obj: any): any {
return obj;
}

/**
* Perform an HTTP request using the custom request function.
* This converts between the HttpClient interface and the CustomRequestFunction interface.
*/
async request(options: HttpClientRequestOptions): Promise<HttpClientResponse> {
const headers = {...(options.headers || {})};

// Add user-agent if set
if (this._userAgent) {
headers['user-agent'] = this._userAgent;
}

let body: string | Buffer | undefined;

// Handle multipart form data - create it manually like HttpClient does
if (options.multipartForm) {
const boundary = '-----------------------------' + Date.now().toString() + Math.random().toString().slice(2);
headers['content-type'] = `multipart/form-data; boundary=${boundary}`;

const encodedBodyParts: Buffer[] = [];
for (const key in options.multipartForm) {
const formObject = options.multipartForm[key];
const content = formObject.content;

let head = `--${boundary}\r\nContent-Disposition: form-data; name="${key}"`;
if (formObject.filename) {
head += `; filename="${formObject.filename}"`;
}
if (formObject.contentType) {
head += `\r\nContent-Type: ${formObject.contentType}`;
}
head += '\r\n\r\n';

encodedBodyParts.push(Buffer.from(head, 'utf8'));
encodedBodyParts.push(Buffer.isBuffer(content) ? content : Buffer.from(String(content), 'utf8'));
encodedBodyParts.push(Buffer.from('\r\n', 'utf8'));
}
encodedBodyParts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
body = Buffer.concat(encodedBodyParts);
}

// Prepare custom request options
const customOptions: CustomRequestOptions = {
method: options.method,
url: options.url,
headers,
queryParams: options.queryString,
body: body // Pass Buffer directly
};

// Execute the custom request function
const customResponse: CustomRequestResponse = await this._customRequestFunction(customOptions);

// Parse response body
let responseBody: string | Buffer = customResponse.body;
if (typeof responseBody !== 'string' && !Buffer.isBuffer(responseBody)) {
responseBody = String(responseBody);
}

let jsonBody: any;
let textBody: string;
let rawBody: Buffer;

if (Buffer.isBuffer(responseBody)) {
rawBody = responseBody;
textBody = responseBody.toString('utf8');
} else {
textBody = responseBody;
rawBody = Buffer.from(responseBody, 'utf8');
}

// Try to parse as JSON if content-type indicates JSON
const responseContentType = this._getHeader(customResponse.headers, 'content-type');
if (responseContentType && responseContentType.includes('application/json')) {
try {
jsonBody = JSON.parse(textBody);
} catch (err) {
// Not valid JSON, leave jsonBody undefined
}
}

// Convert response to HttpClient format
return {
statusCode: customResponse.statusCode,
headers: this._normalizeHeaders(customResponse.headers),
url: customResponse.finalUrl,
jsonBody,
textBody,
rawBody
};
}

/**
* Get a header value case-insensitively
*/
private _getHeader(headers: Record<string, string | string[]>, name: string): string | undefined {
const lowerName = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === lowerName) {
return Array.isArray(value) ? value[0] : value;
}
}
return undefined;
}

/**
* Normalize headers to lowercase keys (HttpClient convention)
*/
private _normalizeHeaders(headers: Record<string, string | string[]>): Record<string, string | string[]> {
const normalized: Record<string, string | string[]> = {};
for (const [key, value] of Object.entries(headers)) {
normalized[key.toLowerCase()] = value;
}
return normalized;
}
}

/**
* Local interfaces matching HttpClient's expected format
*/
interface HttpClientRequestOptions {
method: 'GET' | 'POST';
url: string;
headers?: Record<string, string>;
queryString?: Record<string, any>;
multipartForm?: any;
}

interface HttpClientResponse {
statusCode: number;
headers: Record<string, string | string[]>;
url: string;
jsonBody?: any;
textBody?: string;
rawBody?: Buffer;
}
22 changes: 18 additions & 4 deletions src/LoginSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {TypedEmitter} from 'tiny-typed-emitter';

import AuthenticationClient from './AuthenticationClient';
import {API_HEADERS, decodeJwt, eresultError, defaultUserAgent} from './helpers';
import HttpClientAdapter from './HttpClientAdapter';

import WebApiTransport from './transports/WebApiTransport';
import WebSocketCMTransport from './transports/WebSocketCMTransport';
Expand Down Expand Up @@ -111,6 +112,14 @@ export default class LoginSession extends TypedEmitter<LoginSessionEvents> {
throw new Error('Cannot specify more than one of localAddress, httpProxy, socksProxy, or agent at the same time');
}

// Check if customRequestFunction is used with mutually exclusive options
if (options.customRequestFunction) {
let conflictingOptions = Object.keys(options).filter(k => mutuallyExclusiveOptions.includes(k));
if (conflictingOptions.length > 0) {
throw new Error(`Cannot use customRequestFunction with ${conflictingOptions.join(', ')}. Your custom request function should handle proxy/agent configuration.`);
}
}

let agent:HTTPS.Agent = options.agent || new HTTPS.Agent({keepAlive: true});

if (options.httpProxy) {
Expand All @@ -119,10 +128,15 @@ export default class LoginSession extends TypedEmitter<LoginSessionEvents> {
agent = new SocksProxyAgent(options.socksProxy);
}

this._webClient = new HttpClient({
httpsAgent: agent,
localAddress: options.localAddress
});
// Use custom request function if provided, otherwise use standard HttpClient
if (options.customRequestFunction) {
this._webClient = new HttpClientAdapter(options.customRequestFunction) as any;
} else {
this._webClient = new HttpClient({
httpsAgent: agent,
localAddress: options.localAddress
});
}

this._platformType = platformType;

Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ export {
LoginApprover,
LoginSession
};

export type {
CustomRequestFunction,
CustomRequestOptions,
CustomRequestResponse,
ConstructorOptions,
StartLoginSessionWithCredentialsDetails,
StartSessionResponse,
StartSessionResponseValidAction,
AuthSessionInfo,
ApproveAuthSessionRequest
} from './interfaces-external';
Loading