From 9405cde0c113ed2f0d201dadc98395a9b5e85ecd Mon Sep 17 00:00:00 2001 From: Florrie Date: Tue, 20 Nov 2018 01:04:49 -0400 Subject: Log in with username/password from CLI --- .gitignore | 1 + index.js | 195 ++++++++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 141 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index c2658d7..185d79c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +.scratchSession diff --git a/index.js b/index.js index 001ce39..89cf2b6 100755 --- a/index.js +++ b/index.js @@ -5,19 +5,21 @@ const fetch = require('node-fetch') const fixWS = require('fix-whitespace') const fs = require('fs') const http = require('http') +const prompt = require('prompt') const qs = require('querystring') const url = require('url') const util = require('util') const readFile = util.promisify(fs.readFile) +const writeFile = util.promisify(fs.writeFile) +const doPrompt = util.promisify(prompt.get) // General page size. const limit = 20 -const page = async (request, response, text, title = 'Scratch') => { - const cookies = parseCookies(request) - - const notificationCount = await getMessageCount(cookies.username) +const makePageFunction = (response, userSession) => async (title, text) => { + const { username } = userSession + const notificationCount = await getMessageCount(username) response.setHeader('Content-Type', 'text/html') response.end(fixWS` @@ -30,8 +32,8 @@ const page = async (request, response, text, title = 'Scratch') => {
- ${cookies.username ? fixWS` -

You are logged in as ${templates.user(cookies.username)}. (Log out.) + ${username ? fixWS` +

You are logged in as ${templates.user(username)}. You have ${'' + notificationCount} unread ${pluralize('notification', notificationCount)}.

` : fixWS`

You are not logged in. Log in.

@@ -275,9 +277,9 @@ const templates = { } } -const parseCookies = function(request) { +const parseCookies = function(cookieString) { const list = {} - const rc = request.headers.cookie + const rc = cookieString if (rc) { for (let cookie of rc.split(';')) { @@ -325,10 +327,15 @@ const getExploreProjects = async () => { return shuffle(projects).slice(0, 12) } -const handleRequest = async (request, response) => { +const handleRequest = async (request, response, userSession) => { const { pathname, query } = url.parse(request.url) const queryData = qs.parse(query) - const cookie = parseCookies(request) + const page = makePageFunction(response, userSession) + + const { + username: loginUsername, + apiToken, sessionID, csrfToken + } = userSession const urlParts = pathname.split('/').filter(Boolean) @@ -341,6 +348,7 @@ const handleRequest = async (request, response) => { pageNumber = Math.max(1, pageNumber) } + /* if (compareArr(urlParts, ['login'])) { if (request.method === 'GET') { return page(request, response, fixWS` @@ -393,18 +401,19 @@ const handleRequest = async (request, response) => { `, 'Logged Out') } } + */ if (compareArr(urlParts, ['notifications'])) { - if (!cookie.username) { - return page(request, response, fixWS` + if (!loginUsername) { + return page('Notifications', fixWS`

Sorry, you can't view your notifications until you've logged in.

- `, 'Notifications') + `) } - const notifications = await getNotifications(cookie.username, cookie.token, pageNumber) - const nText = arr => arr.map(n => templates.notification(n, cookie.username)).join('\n') + const notifications = await getNotifications(loginUsername, apiToken, pageNumber) + const nText = arr => arr.map(n => templates.notification(n, loginUsername)).join('\n') - const notificationCount = await getMessageCount(cookie.username) + const notificationCount = await getMessageCount(loginUsername) const currentPageNewCount = Math.max(notificationCount - (limit * (pageNumber - 1)), 0) let listText = '' @@ -427,18 +436,18 @@ const handleRequest = async (request, response) => { ` - return page(request, response, fixWS` + return page('Notifications', fixWS`

Notifications

${listText}

You are on page ${pageNumber}. ${notifications.length === limit && `Next`} ${pageNumber > 1 && `Previous`}

- `, 'Notifications') + `) } if (compareArr(urlParts, ['notifications', 'mark-as-read'])) { if (request.method === 'POST') { - await markNotificationsAsRead(cookie.sessionid, cookie.csrftoken) + await markNotificationsAsRead(sessionID, csrfToken) response.writeHead(302, { 'Location': '/notifications' @@ -453,18 +462,18 @@ const handleRequest = async (request, response) => { if (compareArr(urlParts.slice(0, 2), ['projects', id => /^[0-9]*$/.test(id)])) { const projectID = urlParts[1] - const project = await getProject(projectID, cookie.token) + const project = await getProject(projectID, apiToken) if (project.code === 'NotFound') { response.statusCode = 404 - return page(request, response, fixWS` + return page('Project Not Found', fixWS` 404. Sorry, that project either doesn't exist or isn't shared. - `, 'Project Not Found') + `) } if (urlParts.length === 2) { let parentProjectText = '' if (project.remix.parent) { - const parentProject = await getProject(project.remix.parent, cookie.token) + const parentProject = await getProject(project.remix.parent, apiToken) if (parentProject.code === 'NotFound') { parentProjectText = ` Based on an unshared project.` } else { @@ -475,7 +484,7 @@ const handleRequest = async (request, response) => { const remixes = await fetch(`https://api.scratch.mit.edu/projects/${projectID}/remixes?limit=5`).then(res => res.json()) const remixesText = remixes.map(templates.projectThumbnail).join('\n') - return page(request, response, fixWS` + return page(project.title, fixWS`

${filterHTML(project.title)}

Created by ${templates.user(project.author.username)}.${parentProjectText}

This project's thumbnail

@@ -500,12 +509,12 @@ const handleRequest = async (request, response) => {

See all!

` : ''} - `, project.title) + `) } else if (compareArr(urlParts.slice(2), ['remixes'])) { - return page(request, response, fixWS` + return page(`Remixes of ${project.title}`, fixWS`

Remixes of ${filterHTML(project.title)}

${await templates.projectList(`https://api.scratch.mit.edu/projects/${projectID}/remixes`, pathname, pageNumber)} - `, `Remixes of ${project.title}`) + `) } } @@ -515,16 +524,16 @@ const handleRequest = async (request, response) => { const studio = await getStudio(studioID) if (studio.code === 'NotFound') { response.statusCode = 404 - return page(request, response, fixWS` + return page('Studio Not Found', fixWS` 404. Sorry, that studo doesn't exist. - `, 'Studio Not Found') + `) } if (urlParts.length === 2) { const projects = await fetch(`https://api.scratch.mit.edu/studios/${studioID}/projects?limit=5`).then(res => res.json()) const projectsText = projects.map(templates.projectThumbnail).join('\n') - return page(request, response, fixWS` + return page(studio.title, fixWS`

${filterHTML(studio.title)}

This studio's thumbnail

Description

@@ -538,10 +547,10 @@ const handleRequest = async (request, response) => { ` : `

This studio doesn't have any projects yet!

`} `) } else if (compareArr(urlParts.slice(2), ['projects'])) { - return page(request, response, fixWS` + return page(`Projects in ${studio.title}`, fixWS`

Projects in ${filterHTML(studio.title)}

${await templates.projectList(`https://api.scratch.mit.edu/studios/${studioID}/projects`, pathname, pageNumber)} - `, `Projects in ${studio.title}`) + `) } } @@ -551,16 +560,16 @@ const handleRequest = async (request, response) => { if (user.code === 'NotFound') { response.statusCode = 404 - return page(request, response, fixWS` + return page('User Not Found', fixWS` 404. Sorry, that user doesn't exist. - `, 'User Not Found') + `) } if (urlParts.length === 2) { const projects = await fetch(`https://api.scratch.mit.edu/users/${username}/projects?limit=5`).then(res => res.json()) const projectsText = projects.map(templates.projectThumbnail).join('\n') - return page(request, response, fixWS` + return page(user.username, fixWS`

This user's profile picture ${user.username}

${user.profile.bio ? fixWS`

About Me

@@ -579,19 +588,19 @@ const handleRequest = async (request, response) => { ${projectsText}

See all!

- `, user.username) + `) } else if (compareArr(urlParts.slice(2), ['projects'])) { - return page(request, response, fixWS` + return page(`Projects by ${user.username}`, fixWS`

Projects by ${user.username}

${await templates.projectList(`https://api.scratch.mit.edu/users/${username}/projects`, pathname, pageNumber)} - `, `Projects by ${user.username}`) + `) } } if (compareArr(urlParts, ['explore'])) { const projects = await getExploreProjects() - return page(request, response, fixWS` + return page('Explore', fixWS`

Explore

Here are some randomly picked projects to check out:

` : ''} - `, 'Scratch Homepage') + `) } response.statusCode = 404 - return page(request, response, fixWS` + return page('Page Not Found', fixWS` 404. Sorry, I'm not sure where you are right now. - `, 'Page Not Found') + `) } -const server = http.createServer((request, response) => { - handleRequest(request, response).catch(error => { - console.error(error) - response.statusCode = 500 - return page(request, response, fixWS` - 500. Sorry, there was an internal server error. - `, 'Internal Server Error') +const login = async () => { + let { username, password } = await doPrompt([ + { name: 'username' }, + { name: 'password', hidden: true } + ]) + + const response = await fetch('https://scratch.mit.edu/login/', { + method: 'POST', + body: JSON.stringify({username, password}), + headers: { + 'Cookie': 'scratchcsrftoken=a', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': 'a', + 'Referer': 'https://scratch.mit.edu' + } }) -}) -server.listen(8000) -console.log('Go!') + const [ result ] = await response.json() + + if (!result.success) { + throw {message: result.msg} + } + + username = result.username + + const { scratchsessionsid: sessionID } = parseCookies(response.headers.get('set-cookie')) + + const { user: { token: apiToken } } = await fetch('https://scratch.mit.edu/session', { + headers: { + 'Cookie': `scratchsessionsid=${sessionID}`, + 'X-Requested-With': 'XMLHttpRequest' + } + }).then(res => res.json()) + + // A simple fake CSRF token actually works. + const csrfToken = 'a' + + return {username, sessionID, apiToken, csrfToken} +} + +const loginOrRestore = async () => { + try { + return JSON.parse(await readFile('.scratchSession')) + } catch (error) { + if (error.code === 'ENOENT') { + const userSession = await login() + await writeFile('.scratchSession', JSON.stringify(userSession)) + return userSession + } else { + throw error + } + } +} + +async function main() { + let userSession + + try { + userSession = await loginOrRestore() + } catch (err) { + if (err.message === 'canceled') { + console.log('') + return + } else if (err.message.toLowerCase().startsWith('incorrect')) { + console.log('Sorry, that\'s not the right username or password. Re-run and try again?') + return + } else { + throw err + } + } + + const server = http.createServer((request, response) => { + handleRequest(request, response, userSession).catch(error => { + console.error(error) + response.statusCode = 500 + return makePageFunction(response, userSession)('Internal Server Error', fixWS` + 500. Sorry, there was an internal server error. + `) + }) + }) + + // 127.0.0.1 so that other computers on the network can't connect. + // We don't want anybody to impersonate whoever we're logged in as! + server.listen(8000, '127.0.0.1') + console.log("Alright, it's running: http://127.0.0.1:8000/") +} + +main().catch(err => console.error(err)) -- cgit 1.3.0-6-gf8a5