« get me outta code hell

Log in with username/password from CLI - scratchrlol - Simple HTML-based Scratch client
summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2018-11-20 01:04:49 -0400
committerFlorrie <towerofnix@gmail.com>2018-11-20 01:04:49 -0400
commit9405cde0c113ed2f0d201dadc98395a9b5e85ecd (patch)
tree5f278b5f1208f6d79c9a0fee1fc7bebe00eb2444
parent9b6f0a8f862fe5535f25f0a43c75e582ea55105f (diff)
Log in with username/password from CLI
-rw-r--r--.gitignore1
-rwxr-xr-xindex.js195
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))