CPU time exceeds 10ms

I have a free tier worker as part of an open source project to enable parents to access their child’s homework set on Google Classroom.

A shortened version of the source is below. Currently the median CPU time is about 10.6ms, and I expect this means I’ll soon start getting failed requests. Can I do any of the crypto more efficiently, to reduce CPU time?

addEventListener('fetch', event => {
  event.respondWith(handleRequestAsync(event.request));
});

async function handleRequestAsync(request) {
  const path = new URL(request.url).pathname;

  const corsHeaders = {
    'Access-Control-Allow-Origin': CLIENT_ORIGIN,
    'Access-Control-Allow-Methods': 'GET,OPTIONS',
    'Access-Control-Max-Age': '86400'
  };

  if (path.startsWith('/homework/'))
  {
    const token = path.slice(10);
    const username = await decryptAsync(token);
    if (!validateUsername(username)) return new Response('Not Found', { status: 404, headers: corsHeaders });
    const email = username + '@' + STUDENT_EMAIL_DOMAIN;

    return new Response(await getHomeworksAsync(email), { headers: corsHeaders });
  }
  else
  {
    return new Response('Not Found', { status: 404, headers: corsHeaders });
  }
};

function validateUsername(username) {
  return username && username.length <= 50 && /^[A-Za-z0-9._'-]+$/.test(username);
}

async function getHomeworksAsync(email) {
  const headers = { Authorization: `Bearer ${await getGoogleAuthToken(email)}` };

  const coursesUrl = 'https://classroom.googleapis.com/v1/courses?studentId=me&courseStates=ACTIVE&pageSize=1000&fields=courses(id,name)';
  const coursesResponse = await fetch(coursesUrl, { headers });
  const courses = (await coursesResponse.json()).courses.filter(c => COURSES_SUFFIX === '*' || c.name.endsWith(COURSES_SUFFIX));

  const requests = [];
  for (const course of courses) {
    const courseworkUrl = `https://classroom.googleapis.com/v1/courses/${course.id}/courseWork?orderBy=dueDate%20desc&pageSize=25&fields=courseWork(title,dueDate,description)`;
    requests.push(fetch(courseworkUrl, { headers }).then(resp => resp.json()));
  }
  const courseworkListResults = await Promise.all(requests);
  const today = new Date().setHours(0,0,0,0);
  const oneWeek = 6.048e+8;
  const homeworks = [];
  for (let i = 0; i < courseworkListResults.length; i++) {
    const courseworkItems = courseworkListResults[i].courseWork;
    if (!courseworkItems) continue;
    for (const hw of courseworkItems) {
      if (!hw.dueDate || !hw.dueDate.year || !hw.dueDate.month || !hw.dueDate.year) continue;
      var date = new Date(hw.dueDate.year, hw.dueDate.month - 1, hw.dueDate.day);
      if (date > today + 3 * oneWeek) continue;
      if (date < today - oneWeek) break;
      homeworks.push({ title: hw.title.trim(), description: hw.description?.trim() ?? '', dueDate: date, subject: courses[i].name });
    }
  }

  return JSON.stringify(homeworks.sort((a, b) => (a.dueDate - b.dueDate)));
};

async function getGoogleAuthToken(email) {
  const objectToBase64url = obj => arrayBufferToBase64Url(new TextEncoder().encode(JSON.stringify(obj)));
  const arrayBufferToBase64Url = buffer => byteArrayToBase64(new Uint8Array(buffer));

  async function sign(content, signingKey) {
    const bytes = asciiToByteArray(content);
    const plainKey = signingKey.replace('-----BEGIN PRIVATE KEY-----', '').replace('-----END PRIVATE KEY-----', '').replace(/(\r\n|\n|\r)/gm, '');
    const keyData = asciiToByteArray(atob(plainKey));
    const key = await crypto.subtle.importKey('pkcs8', keyData, { name: 'RSASSA-PKCS1-V1_5', hash: { name: 'SHA-256' } }, false, ['sign']);
    const signature = await crypto.subtle.sign({ name: 'RSASSA-PKCS1-V1_5' }, key, bytes);
    return arrayBufferToBase64Url(signature);
  }
  const assertiontime = Math.round(Date.now() / 1000);
  const claimset = objectToBase64url({
    iss: GOOGLE_SERVICE_ACCOUNT_EMAIL,
    scope: 'https://www.googleapis.com/auth/classroom.courses.readonly https://www.googleapis.com/auth/classroom.coursework.me.readonly',
    aud: 'https://oauth2.googleapis.com/token',
    exp: assertiontime + 3600,
    iat: assertiontime,
    sub: email
  });
  const jwtHeader = objectToBase64url({ alg: 'RS256', typ: 'JWT' });
  const jwtUnsigned = jwtHeader + '.' + claimset;
  const signedJwt = jwtUnsigned + '.' + await sign(jwtUnsigned, GOOGLE_PRIVATE_KEY);
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Cache-Control': 'no-cache', 'Host': 'oauth2.googleapis.com' },
    body: 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=' + signedJwt
  });
  const json = await response.json();
  return json.access_token;
}

async function importKeyAsync(str) {
  const jwk = { kty: 'oct', k: str, alg: 'A256CBC', ext: true };
  return crypto.subtle.importKey('jwk', jwk, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt']);
}

async function decryptAsync(str) {
  const key = await importKeyAsync(ENCRYPTION_KEY);
  const iv = base64ToByteArray(ENCRYPTION_IV);
  try {
    const bytes = base64ToByteArray(str);
    const plainBytes = new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, bytes));
    return byteArrayToAscii(plainBytes);
  } catch {
    return null;
  }
};

function byteArrayToAscii(bytes) {
  return String.fromCharCode.apply(null, bytes);
}

function base64ToByteArray(str) {
  if (str.length % 4 === 2) str += '==';
  else if (str.length % 4 === 3) str += '=';
  const chars = atob(str.replace(/_/g, '/').replace(/\-/g, '+'));
  return asciiToByteArray(chars);
}

function byteArrayToBase64(bytes) {
  const chars = byteArrayToAscii(bytes);
  return btoa(chars).replace(/\//g, '_').replace(/\+/g, '-').replace(/=/g, '');
}

Honestly, it would be odd if somebody actually applies optimizations to the code, its a complex task that needs benchmarking and an actual way to test the code properly :sweat_smile:.

The good part is that you miiight be able to get this sponsored. reach out to os-sponsorship AT cloudflare.com and they will verify whether you are eligible for an sponsorship or not.

3 Likes