Как внедрить JavaScript в существующий HTML-ответ с помощью node.js и работников cloudflare - PullRequest
1 голос
/ 19 июня 2019

У меня есть тщеславный URL, указывающий на GitBook. GitBook не поддерживает вставку произвольных фрагментов JavaScript. На данный момент GitBook имеет только 4 "интеграции".

Я мог бы выполнить маршрутизацию через свой виртуальный сервер для достижения этой цели, но у меня есть CloudFlare, и я хочу попробовать рабочих. (Javascript работает на границе CDN).

Рабочая среда CloudFlare значительно упрощает внедрение заголовков, но очевидного способа сделать это не существует.

1 Ответ

1 голос
/ 19 июня 2019

Важно обрабатывать с помощью TransformStream, чтобы обработка была асинхронной и не требовала буферизации памяти (для масштабируемости и минимизации GC) - есть только 5 мс времени CPU.

Обзор :

  • Чтобы использовать для себя, измените строки forHeadStart, forHeadEnd и forBodyEnd.
  • Этот подход deferredInjection является рекомендуемым способом, который минимизирует время процессора для рабочего.Это более эффективно, потому что для этого нужно только проанализировать самое начало HTML.Другой подход требует синтаксического анализа всего раздела head для headInjection, и если вы используете bodyInjection, ему практически необходимо проанализировать весь html-ответ.
  • Подход deferredInjection работает путем внедрения содержимого в начало тега head, а затем на стороне клиента во время выполнения ваш HTML-контент будет развернут в нужных местах.
  • Вы можете ввестинепосредственно при необходимости, используя headInjection и / или bodyInjection.Раскомментирование связанного кода, включая код в injectScripts и установку строк для тегируемых байтов, которые будут закодированы.
  • Это решение будет анализировать только типы содержимого HTML
  • Это решение работает непосредственно с байтами (нестроки) для лучшей эффективности.Поиск байтов строк конечных тегов.
  • Вы можете указать больше конечных тегов, но обычно вам не нужно указывать больше, чем эти два
  • Обрабатывает данные с потоковой передачей (вся строка HTML не кэшируется в памяти).Это снижает пиковое использование памяти и ускоряет время до первого байта.
  • Обрабатывает случай редкого края, когда закрывающий тег находится на границе чтения текста.Я полагаю, что граница может возникать каждые ~ 1000 байтов (TCP-пакеты 1000-1500 байтов каждый), и это может варьироваться из-за сжатия gzip.
  • Хранит код синтаксического анализа инъекций отдельно для кода, чтобы просто пересылать остальноеЯсность.
  • Вы можете отключить второй инжектор тега body, закомментировав его, если он вам не нужен, - это ускорит обработку.
  • Я протестировал этот точный код для себя ионо работает.Могут быть оставшиеся ошибки (в зависимости от местоположения закрывающего тега и в зависимости от того, отвечает ли ваш сервер частичными HTML-шаблонами (только текст сообщения)).Возможно, я исправил один сегодня 2019-06-28

Код

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

/**
 * Fetch and log a request
 * @param {Request} request
 */
async function handleRequest(request) {
  const response = await fetch(request);

  var ctype = response.headers.get('content-type');
  if (ctype.startsWith('text/html') === false)
    return response; //Only parse html body

  let { readable, writable } = new TransformStream();
  let promise = injectScripts(response.body, writable);
  return new Response(readable, response);
}

let encoder = new TextEncoder('utf-8');

let deferredInjection = function() {
    let forHeadStart = `<script>var test = 1; //Start of head section</script>`;
    let forHeadEnd = `<script>var test = 2; //End of head section</script>`;
    let forBodyEnd = `<script>var test = 3; //End of body section</script><button>click</button>`;

    let helper = `
    ${forHeadStart}
    <script>
        function appendHtmlTo(element, htmlContent) {
            var temp = document.createElement('div');
            temp.innerHTML = htmlContent;
            while (temp.firstChild) {
                element.appendChild(temp.firstChild);
            };
        }

        let forHeadEnd = "${ btoa(forHeadEnd) }";
        let forBodyEnd = "${ btoa(forBodyEnd) }";

        if (forHeadEnd.length > 0) appendHtmlTo(document.head, atob(forHeadEnd)); 
    if (forBodyEnd.length > 0) window.onload = function() {
      appendHtmlTo(document.body, atob(forBodyEnd));
    };

    </script>
    `;
    return {
        forInjection: encoder.encode(helper),
        tagBytes: encoder.encode("<head>"),
        insertAfterTag: true
    };

}();

// let headInjection = {
    // forInjection: encoder.encode("<script>var test = 1;</script>"),
    // tagBytes: encoder.encode("</head>"), //case sensitive
    // insertAfterTag: false
// };
// let bodyInjection = {
    // forInjection: encoder.encode("<script>var test = 1;</script>"),
    // tagBytes: encoder.encode("</body>"), //case sensitive
    // insertAfterTag: false
// }

//console.log(bodyTagBytes);
encoder = null;

async function injectScripts(readable, writable) {
  let processingState = {
    readStream: readable,
    writeStream: writable,
    reader: readable.getReader(),
    writer: writable.getWriter(),
    leftOvers: null, //data left over after a closing tag is found
    inputDone: false,
    result: {charactersFound: 0, foundIndex: -1, afterHeadTag: -1} //Reused object for the duration of the request
  };


  await parseForInjection(processingState, deferredInjection);

  //await parseForInjection(processingState, headInjection);

  //await parseForInjection(processingState, bodyInjection);

  await forwardTheRest(processingState);      
}



///Return object will have foundIndex: -1, if there is no match, and no partial match at the end of the array
///If there is an exact match, return object will have charactersFound:(tagBytes.Length)
///If there is a partial match at the end of the array, return object charactersFound will be < (tagBytes.Length)
///The result object needs to be passed in to reduce Garbage Collection - we can reuse the object
function searchByteArrayChunkForClosingTag(chunk, tagBytes, result)
{   
  //console.log('search');
    let searchStart = 0;
  //console.log(tagBytes.length);
    //console.log(chunk.length);

    for (;;) {
        result.charactersFound = 0;
        result.foundIndex = -1;
        result.afterHeadTag = -1;
    //console.log(result);

        let sweepIndex = chunk.indexOf(tagBytes[0], searchStart);
        if (sweepIndex === -1)
            return; //Definitely not found

        result.foundIndex = sweepIndex;
        sweepIndex++;
        searchStart = sweepIndex; //where we start searching from next
        result.charactersFound++;   
        result.afterHeadTag = sweepIndex;

    //console.log(result);

        for (let i = 1; i < tagBytes.length; i++)
        {
            if (sweepIndex === chunk.length) return; //Partial match
            if (chunk[sweepIndex++] !== tagBytes[i]) break; //Failed to match (even partially to boundary)
            result.charactersFound++;
            result.afterHeadTag = sweepIndex; //Because we work around the actual found tag in case it's across a boundary
        }   

    if (result.charactersFound === tagBytes.length)
          return; //Found
    }

}

function continueSearchByteArrayChunkForClosingTag(chunk, tagBytes, lastSplitResult, result)
{
  //console.log('continue');
    //Finish the search (no need to check the last buffer at all)
    //console.log('finish the search');
    result.charactersFound = lastSplitResult.charactersFound; //We'll be building on the progress from the lastSplitResult
    result.foundIndex = (-1 * result.charactersFound); //This won't be used, but a negative value is indicative of chunk spanning
    let sweepIndex = 0;
    result.afterHeadTag = 0;
    for (let i = lastSplitResult.charactersFound - 1; i < tagBytes.length; i++) //Zero-based
    {
        if (sweepIndex === chunk.length) return result; //So we support working on a chunk that's smaller than the tagBytes search size
        if (chunk[sweepIndex++] !== tagBytes[i]) break;
        result.charactersFound++;
        result.afterHeadTag = sweepIndex;
    }
}

function continueOrNewSearch(chunk, tagBytes, lastSplitResult, result)
{
  //console.log('continueOrNewSearch');
      if (lastSplitResult == null)
          searchByteArrayChunkForClosingTag(chunk, tagBytes, result);
      else
      {
          continueSearchByteArrayChunkForClosingTag(chunk, tagBytes, lastSplitResult, result);
        if (result.charactersFound === tagBytes.length)
            return result;
        else
            return searchByteArrayChunkForClosingTag(chunk, tagBytes, result); //Keep searching onward
      }
}

async function parseForInjection(processingState, injectionJob)
{
  if (processingState.inputDone) return; //Very edge case: Somehow </head> is never found?            
  if (!injectionJob) return;
  if (!injectionJob.tagBytes) return;
  if (!injectionJob.forInjection) return;

  let reader = processingState.reader;
  let writer = processingState.writer;
  let result = processingState.result;
  let tagBytes = injectionJob.tagBytes;
  //(reader, writer, tagBytes, forInjection)

  let lastSplitResult = null;
  let chunk = null;
  processingState.inputDone = false;
  for (;;) {
    if (processingState.leftOvers)
      {
      chunk = processingState.leftOvers;
      processingState.leftOvers = null;
      }
      else
      {
      let readerResult = await reader.read();
      chunk = readerResult.value;
      processingState.inputDone = readerResult.done;
      }

      if (processingState.inputDone) {
        if (lastSplitResult !== null) {
            //Very edge case: Somehow tagBytes is never found?            
            console.log('edge');
                  throw 'tag not found'; //Causing the system to fall back to the direct request
        }
        await writer.close();
        return true;
      }   
      //console.log(value.length);

        continueOrNewSearch(chunk, tagBytes, lastSplitResult, result)
      //console.log(result);

      if (result.charactersFound === tagBytes.length) //Complete match
      {
        //Inject
        //console.log('inject');
        if (result.foundIndex > 0)
        {
          let partValue = chunk.slice(0, result.foundIndex);
          //console.log(partValue);
          await writer.write(partValue);
        }
        console.log('injected');
        if (parseForInjection.insertAfterTag)
        {
            await writer.write(injectionJob.forInjection);
            await writer.write(injectionJob.tagBytes);
        }
        else
        {
            await writer.write(injectionJob.tagBytes);
            await writer.write(injectionJob.forInjection);
        }
        let remainder = chunk.slice(result.afterHeadTag, chunk.length - 1);
        processingState.leftOvers = remainder;
        lastSplitResult = null;
        return;
      }

      if (lastSplitResult !== null)
      {
        //console.log('no match over boundary');
        //The remainder wasn't found, so write the partial match from before (maybe `<` or `</`)
        let failedLastBit = injectionJob.tagBytes.slice(0, lastSplitResult.charactersFound);
        await writer.write(failedLastBit);
        lastSplitResult = null;
      }

      if (result.charactersFound === 0)
      {
        //console.log('not found')
        await writer.write(chunk);
        continue;
      }

      if (result.charactersFound < tagBytes.length)
      {
        //console.log('boundary: ' + result.charactersFound);
        lastSplitResult = result;
        let partValue = chunk.slice(0, result.foundIndex);
        //console.log(partValue);
        await writer.write(partValue);
        continue;
      }
  }
}

async function forwardTheRest(processingState)
{
  try
  {
  if (processingState.inputDone) return; //Very edge case: Somehow </head> is never found?            

  if (processingState.leftOvers)
  {
    chunk = processingState.leftOvers;
    await processingState.writer.write(chunk);
  }

  processingState.reader.releaseLock();
  processingState.writer.releaseLock();

  await processingState.readStream.pipeTo(processingState.writeStream);

  //Should there be an explicit close method called? I couldn't find one
  }
  catch (e)
  {
    console.log(e);
  }
}

Дальнейшее объяснение работы напрямую с (utf-8) байтов:

  • Работает только со значениями байтов.Это возможно, по крайней мере, путем поиска первого отличительного байта utf-8 символа (<128 и> 192).Но в этом случае мы ищем </head>, который состоит из байтов меньше 128, с которыми очень легко работать.
  • Учитывая природу поиска utf-8 (который являетсяхитрее), это должно работать с ['utf-8', 'utf8', 'iso-8859-1', 'us-ascii'].Вам нужно будет изменить кодировщик сниппета, чтобы он соответствовал.
  • Это не полностью проверено.Граничный случай, не сработал для меня.В идеале, у нас должна быть испытательная установка для основных функций
  • , благодаря Кентону Варде, который бросил мне вызов
  • Пожалуйста, дайте мне знать, есть ли у CloudFlare способ выполнить pipeTo в функции forwardTheRest
  • Возможно, вы найдете continueOrNewSearch и две его подфункции интересным подходом к поиску многобайтовых данных через границу фрагмента.До границы мы просто считаем, сколько байтов найдено.Нет необходимости хранить эти байты (мы знаем, что они есть).Затем на следующем фрагменте мы продолжаем с того места, где остановились.Мы всегда обрезаем буфер массива вокруг заголовка и проверяем, что мы записываем байты заголовка (используя tagBytes)
...