Alexaの認証を延長するもの
This commit is contained in:
239
alexa-api/auth4-core.js
Normal file
239
alexa-api/auth4-core.js
Normal file
@@ -0,0 +1,239 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ALEXA_LOGIN_URL =
|
||||
'https://www.amazon.co.jp/ap/signin?' +
|
||||
new URLSearchParams({
|
||||
'openid.assoc_handle': 'amzn_dp_project_dee_jp',
|
||||
'openid.mode': 'checkid_setup',
|
||||
'openid.ns': 'http://specs.openid.net/auth/2.0',
|
||||
'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select',
|
||||
'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select',
|
||||
'openid.return_to': 'https://alexa.amazon.co.jp/api/apps/v1/token',
|
||||
'pageId': 'amzn_dp_project_dee_jp',
|
||||
}).toString();
|
||||
|
||||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36';
|
||||
|
||||
function createLogger(logger) {
|
||||
if (!logger) return function() {};
|
||||
if (typeof logger === 'function') return logger;
|
||||
if (typeof logger.log === 'function') {
|
||||
return function(message) {
|
||||
logger.log(message);
|
||||
};
|
||||
}
|
||||
return function() {};
|
||||
}
|
||||
|
||||
function createRequestState() {
|
||||
const cookieJar = {};
|
||||
|
||||
function setCookies(setCookieHeaders) {
|
||||
if (!setCookieHeaders) return;
|
||||
const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders];
|
||||
for (const header of headers) {
|
||||
const parts = header.split(';');
|
||||
const kv = parts[0] || '';
|
||||
const index = kv.indexOf('=');
|
||||
if (index <= 0) continue;
|
||||
const key = kv.slice(0, index).trim();
|
||||
const value = kv.slice(index + 1).trim();
|
||||
cookieJar[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function getCookieHeader() {
|
||||
return Object.entries(cookieJar)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
function request(url, options) {
|
||||
const opts = options || {};
|
||||
return new Promise((resolve, reject) => {
|
||||
const parsed = new URL(url);
|
||||
const reqOpts = {
|
||||
hostname: parsed.hostname,
|
||||
path: parsed.pathname + parsed.search,
|
||||
method: opts.method || 'GET',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept-Language': 'ja-JP,ja;q=0.9',
|
||||
'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8',
|
||||
'Cookie': getCookieHeader(),
|
||||
...(opts.headers || {}),
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(reqOpts, (res) => {
|
||||
setCookies(res.headers['set-cookie']);
|
||||
let body = '';
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({ status: res.statusCode, headers: res.headers, body });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (opts.body) req.write(opts.body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cookieJar,
|
||||
getCookieHeader,
|
||||
request,
|
||||
};
|
||||
}
|
||||
|
||||
function extractHiddenFields(html) {
|
||||
const fields = {};
|
||||
const re = /<input[^>]+type=["']?hidden["']?[^>]*>/gi;
|
||||
let match;
|
||||
while ((match = re.exec(html)) !== null) {
|
||||
const tag = match[0];
|
||||
const name = (tag.match(/name=["']([^"']+)["']/) || [])[1];
|
||||
const value = (tag.match(/value=["']([^"']*)["']/) || ['', ''])[1];
|
||||
if (name) fields[name] = value;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function extractFormAction(html) {
|
||||
const match = html.match(/id="ap_login_form"[^>]+action="([^"]+)"/);
|
||||
if (match) return match[1].replace(/&/g, '&');
|
||||
const match2 = html.match(/name="signIn"[^>]+action="([^"]+)"/);
|
||||
if (match2) return match2[1].replace(/&/g, '&');
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildFailureMessage(current, cookieJar) {
|
||||
const keys = Object.keys(cookieJar).join(', ') || '(none)';
|
||||
const hints = [];
|
||||
if (current.body && (current.body.includes('captcha') || current.body.includes('CAPTCHA'))) {
|
||||
hints.push('CAPTCHA が要求されています。少し待ってから再試行してください。');
|
||||
}
|
||||
if (current.body && current.body.includes('auth-mfa-form')) {
|
||||
hints.push('MFA が要求されています。このスクリプトだけでは完了できません。');
|
||||
}
|
||||
if (current.body && current.body.includes('password') && current.body.includes('error')) {
|
||||
hints.push('パスワードが間違っている可能性があります。');
|
||||
}
|
||||
|
||||
const message = [
|
||||
'認証に失敗しました。取得できた Cookie に認証トークンが含まれていません。',
|
||||
`取得済み Cookie キー: ${keys}`,
|
||||
...hints,
|
||||
].join('\n');
|
||||
|
||||
const error = new Error(message);
|
||||
error.code = 'ALEXA_AUTH_FAILED';
|
||||
error.hints = hints;
|
||||
return error;
|
||||
}
|
||||
|
||||
async function fetchAlexaCookie(options) {
|
||||
const opts = options || {};
|
||||
const email = opts.email;
|
||||
const password = opts.password;
|
||||
const log = createLogger(opts.logger);
|
||||
|
||||
if (!email || !password) {
|
||||
const error = new Error('email と password は必須です');
|
||||
error.code = 'MISSING_CREDENTIALS';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const state = createRequestState();
|
||||
const request = state.request;
|
||||
const cookieJar = state.cookieJar;
|
||||
|
||||
log('[1] ログインページ取得中...');
|
||||
const page1 = await request(ALEXA_LOGIN_URL);
|
||||
log(` Status: ${page1.status}, Cookies: ${Object.keys(cookieJar).join(', ')}`);
|
||||
|
||||
if (page1.status !== 200) {
|
||||
const error = new Error(`ログインページ取得失敗: ${page1.status}`);
|
||||
error.code = 'LOGIN_PAGE_FETCH_FAILED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const action = extractFormAction(page1.body);
|
||||
const hidden = extractHiddenFields(page1.body);
|
||||
|
||||
if (!action) {
|
||||
const error = new Error('ログインフォームが見つかりません。Amazon のログイン画面仕様が変わった可能性があります。');
|
||||
error.code = 'LOGIN_FORM_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
log(`[2] フォーム送信先: ${action}`);
|
||||
log(` Hidden fields: ${Object.keys(hidden).join(', ')}`);
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
...hidden,
|
||||
email: email,
|
||||
password: password,
|
||||
rememberMe: 'true',
|
||||
}).toString();
|
||||
|
||||
log('[3] 認証送信中...');
|
||||
const page2 = await request(action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Referer': ALEXA_LOGIN_URL,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
log(` Status: ${page2.status}`);
|
||||
log(` Location: ${page2.headers.location || '(none)'}`);
|
||||
log(` Cookies after login: ${Object.keys(cookieJar).join(', ')}`);
|
||||
|
||||
let current = page2;
|
||||
let redirectCount = 0;
|
||||
while (current.status >= 300 && current.status < 400 && current.headers.location && redirectCount < 10) {
|
||||
const location = current.headers.location;
|
||||
const nextUrl = location.startsWith('http') ? location : `https://www.amazon.co.jp${location}`;
|
||||
log(`[${4 + redirectCount}] Redirect -> ${nextUrl.substring(0, 80)}...`);
|
||||
current = await request(nextUrl);
|
||||
log(` Status: ${current.status}, New Cookies: ${Object.keys(cookieJar).join(', ')}`);
|
||||
redirectCount += 1;
|
||||
}
|
||||
|
||||
const cookie = state.getCookieHeader();
|
||||
const hasAlexaToken = cookie.includes('at-acbjp') || cookie.includes('session-token');
|
||||
if (!hasAlexaToken) throw buildFailureMessage(current, cookieJar);
|
||||
|
||||
return {
|
||||
cookie,
|
||||
cookieLength: cookie.length,
|
||||
cookieKeys: Object.keys(cookieJar),
|
||||
};
|
||||
}
|
||||
|
||||
function saveCookieToEnv(cookie, envPath) {
|
||||
const targetEnvPath = envPath || path.join(__dirname, '.env');
|
||||
fs.writeFileSync(targetEnvPath, `ALEXA_COOKIE=${cookie}\n`);
|
||||
return targetEnvPath;
|
||||
}
|
||||
|
||||
async function fetchAlexaCookieAndSave(options) {
|
||||
const result = await fetchAlexaCookie(options);
|
||||
const envPath = saveCookieToEnv(result.cookie, options && options.envPath);
|
||||
return {
|
||||
...result,
|
||||
envPath,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAlexaCookie,
|
||||
fetchAlexaCookieAndSave,
|
||||
saveCookieToEnv,
|
||||
};
|
||||
Reference in New Issue
Block a user