diff options
author | Florrie <towerofnix@gmail.com> | 2018-11-20 01:04:49 -0400 |
---|---|---|
committer | Florrie <towerofnix@gmail.com> | 2018-11-20 01:04:49 -0400 |
commit | 9405cde0c113ed2f0d201dadc98395a9b5e85ecd (patch) | |
tree | 5f278b5f1208f6d79c9a0fee1fc7bebe00eb2444 | |
parent | 9b6f0a8f862fe5535f25f0a43c75e582ea55105f (diff) |
Log in with username/password from CLI
-rw-r--r-- | .gitignore | 1 | ||||
-rwxr-xr-x | 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') => { </head> <body> <header> - ${cookies.username ? fixWS` - <p>You are logged in as ${templates.user(cookies.username)}. (<a href="/logout">Log out.</a>) + ${username ? fixWS` + <p>You are logged in as ${templates.user(username)}. You have ${'' + notificationCount} unread <a href="/notifications">${pluralize('notification', notificationCount)}</a>.</p>` : fixWS` <p>You are not logged in. <a href="/login">Log in.</a></p> @@ -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` <p>Sorry, you can't view your notifications until you've <a href="/login">logged in</a>.</p> - `, '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) => { </ul> ` - return page(request, response, fixWS` + return page('Notifications', fixWS` <h1>Notifications</h1> ${listText} <p>You are on page ${pageNumber}. ${notifications.length === limit && `<a href="/notifications?page=${pageNumber + 1}">Next</a>`} ${pageNumber > 1 && `<a href="/notifications?page=${pageNumber - 1}">Previous</a>`}</p> - `, '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` <h1>${filterHTML(project.title)}</h1> <p>Created by ${templates.user(project.author.username)}.${parentProjectText}</p> <p><img src="${project.image}" alt="This project's thumbnail"></p> @@ -500,12 +509,12 @@ const handleRequest = async (request, response) => { </ul> <p><a href="/projects/${project.id}/remixes">See all!</a></p> ` : ''} - `, project.title) + `) } else if (compareArr(urlParts.slice(2), ['remixes'])) { - return page(request, response, fixWS` + return page(`Remixes of ${project.title}`, fixWS` <h1>Remixes of ${filterHTML(project.title)}</h1> ${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` <h1>${filterHTML(studio.title)}</h1> <p><img src="${studio.image}" alt="This studio's thumbnail"></p> <h2>Description</h2> @@ -538,10 +547,10 @@ const handleRequest = async (request, response) => { ` : `<p>This studio doesn't have any projects yet!</p>`} `) } else if (compareArr(urlParts.slice(2), ['projects'])) { - return page(request, response, fixWS` + return page(`Projects in ${studio.title}`, fixWS` <h1>Projects in ${filterHTML(studio.title)}</h1> ${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` <h1><img src="${user.profile.images['60x60']}" height="60px" alt="This user's profile picture"> ${user.username}</h1> ${user.profile.bio ? fixWS` <h2>About Me</h2> @@ -579,19 +588,19 @@ const handleRequest = async (request, response) => { ${projectsText} </ul> <p><a href="/users/${username}/projects">See all!</a></p> - `, user.username) + `) } else if (compareArr(urlParts.slice(2), ['projects'])) { - return page(request, response, fixWS` + return page(`Projects by ${user.username}`, fixWS` <h1>Projects by ${user.username}</h1> ${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` <h1>Explore</h1> <p>Here are some randomly picked projects to check out:</p> <ul class="thumb-list"> @@ -614,12 +623,12 @@ const handleRequest = async (request, response) => { if (urlParts.length === 0) { let activityText = '' - if (cookie.username) { - const activity = await fetch(`https://api.scratch.mit.edu/users/${cookie.username}/following/users/activity?limit=8&x-token=${cookie.token}`).then(res => res.json()) + if (loginUsername) { + const activity = await fetch(`https://api.scratch.mit.edu/users/${loginUsername}/following/users/activity?limit=8&x-token=${apiToken}`).then(res => res.json()) activityText = activity.map(templates.activity).join('\n') } - return page(request, response, fixWS` + return page('Scratch', fixWS` <h1>Scratch Unofficial HTML Client</h1> <p>Welcome!</p> ${activityText ? fixWS` @@ -628,24 +637,100 @@ const handleRequest = async (request, response) => { ${activityText} </ul> ` : ''} - `, '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)) |