Узловые HTTP-серверы для обслуживания статических файлов

Одним из наиболее фундаментальных способов использования HTTP-сервера является предоставление браузеру пользователя статических файлов, таких как CSS, JavaScript или файлы изображений. Помимо обычного использования браузера, есть тысячи других причин, по которым вам может понадобиться обслуживать статические файлы, например, для загрузки музыки или научных данных. В любом случае вам нужно придумать простой способ, позволяющий пользователю загружать эти файлы с вашего сервера. Один из простых способов сделать это - создать HTTP-сервер Node. Как вы, наверное, знаете, Node.js отлично справляется с

Одним из наиболее фундаментальных способов использования HTTP-сервера является предоставление браузеру пользователя статических файлов, таких как CSS, JavaScript или файлы изображений. Помимо обычного использования браузера, есть тысячи других причин, по которым вам может понадобиться обслуживать статические файлы, например, для загрузки музыки или научных данных. В любом случае вам нужно придумать простой способ, позволяющий пользователю загружать эти файлы с вашего сервера.

Один из простых способов сделать это - создать HTTP-сервер Node. Как вы, наверное, знаете, Node.js отлично справляется с задачами с интенсивным вводом-выводом, что делает его здесь естественным выбором. Вы можете создать свой собственный простой HTTP-сервер из базового модуля http , поставляемого с Node, или использовать популярный пакет serve-static , который предоставляет множество общих функций статического файлового сервера.

Конечная цель нашего статического сервера - позволить пользователю указать путь к файлу в URL-адресе и вернуть этот файл как содержимое страницы. Однако пользователь не должен иметь возможность указывать какой-либо путь на нашем сервере, иначе злоумышленник может попытаться воспользоваться неправильно настроенной системой и украсть конфиденциальную информацию. Простая атака может выглядеть так: localhost:8080/etc/shadow . Здесь злоумышленник запрашивает файл /etc/shadow Чтобы предотвратить подобные атаки, мы должны иметь возможность указать серверу, чтобы он разрешал пользователю загружать только определенные файлы или только файлы из определенных каталогов (например, /var/www/my-website/public ).

Создание собственного

Этот раздел предназначен для тех из вас, кому нужен более индивидуальный вариант, или для тех, кто хочет узнать, как работают статические серверы (или просто серверы в целом). Если у вас довольно распространенный вариант использования, вам лучше перейти к следующему разделу и начать работать непосредственно с модулем serve-static

Хотя создание собственного сервера из http требует небольшой работы, может быть очень полезно показать вам, как серверы работают под ним, что включает в себя компромиссы для производительности и безопасности, которые необходимо учитывать. Хотя довольно легко создать свой собственный статический сервер Node, используя только встроенный http , поэтому нам не нужно слишком углубляться во внутреннее устройство HTTP-сервера.

Очевидно, что http будет не так прост в использовании, как что-то вроде Express, но это отличная отправная точка в качестве HTTP-сервера. Здесь я покажу вам, как создать простой статический HTTP-сервер, который затем можно добавить и настроить по своему вкусу.

Начнем с инициализации и запуска нашего HTTP-сервера:

 "use strict"; 
 
 var http = require('http'); 
 
 var staticServe = function(req, res) { 
 res.statusCode = 200; 
 res.write('ok'); 
 return res.end(); 
 }; 
 
 var httpServer = http.createServer(staticServe); 
 
 httpServer.listen(8080); 

Если вы запустите этот код и перейдете к localhost:8080 в своем браузере, то все, что вы увидите, будет «ОК» на экране. Этот код обрабатывает все запросы к адресу localhost:8080 . Даже для некорневых путей, таких как localhost:8080/some/url/path вы все равно получите тот же ответ. Таким образом, каждый запрос, полученный сервером, обрабатывается staticServe , в которой и будет находиться основная часть нашей статической серверной логики.

Следующим шагом является получение от пользователя пути к файлу, который мы получаем с помощью URL-пути. Вероятно, было бы плохой идеей позволить пользователю указывать абсолютный путь в нашей системе по нескольким причинам:

  • Сервер не должен раскрывать детали базовой операционной системы.
  • Пользователь должен быть ограничен в файлах, которые он может загрузить, чтобы он не мог попытаться получить доступ к конфиденциальным файлам, таким как /etc/shadow .
  • URL-адрес не должен требовать повторяющихся частей пути к файлу (например, корень каталога: /var/www/my-website/public/... )

Учитывая эти требования, нам нужно указать базовый путь для сервера, а затем использовать данный URL-адрес как относительный путь от базового. Для этого мы можем использовать функции .resolve() и .join() из модуля пути узла:

 "use strict"; 
 
 var path = require('path'); 
 var http = require('http'); 
 
 var staticBasePath = './static'; 
 
 var staticServe = function(req, res) { 
 var resolvedBase = path.resolve(staticBasePath); 
 var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); 
 var fileLoc = path.join(resolvedBase, safeSuffix); 
 
 res.statusCode = 200; 
 
 res.write(fileLoc); 
 return res.end(); 
 }; 
 
 var httpServer = http.createServer(staticServe); 
 
 httpServer.listen(8080); 

Здесь мы создаем полный путь к файлу, используя базовый путь, staticBasePath и заданный URL-адрес, который затем выводим для пользователя.

Теперь, если вы перейдете к тому же localhost:8080/some/url/path , вы должны увидеть следующий текст, напечатанный в браузере:

 /Users/scott/Projects/static-server/static/some/url/path 

Имейте в виду, что ваш путь к файлу, скорее всего, будет отличаться от моего, в зависимости от вашей ОС, имени пользователя и пути к проекту. Самый важный вывод - это несколько последних показанных каталогов ( static/some/url/path ).

Удалив "." и '..' из req.url , а затем с помощью .resolve() , .normalize() и .join() мы можем ограничить пользователя доступом только к файлам в каталоге ./static Даже если вы попытаетесь обратиться к родительскому каталогу с помощью .. вы не сможете получить доступ к каким-либо родительским каталогам за пределами 'static', поэтому другие наши данные в безопасности.

Примечание : наш код соединения путей не был тщательно протестирован и должен считаться небезопасным без надлежащего тестирования самостоятельно!

Теперь, когда мы ограничили путь только возвращаемыми файлами в данном каталоге, мы можем начать обслуживание самих файлов. Для этого мы просто воспользуемся fs.readFile() для загрузки содержимого файла.

Чтобы лучше обслуживать содержимое, все, что нам нужно сделать, это отправить файл пользователю с помощью res.write(content) , как мы это делали ранее с путем к файлу. Если мы не можем найти запрошенный файл, вместо этого мы вернем ошибку 404.

 "use strict"; 
 
 var fs = require('fs'); 
 var path = require('path'); 
 var http = require('http'); 
 
 var staticBasePath = './static'; 
 
 var staticServe = function(req, res) { 
 var resolvedBase = path.resolve(staticBasePath); 
 var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); 
 var fileLoc = path.join(resolvedBase, safeSuffix); 
 
 fs.readFile(fileLoc, function(err, data) { 
 if (err) { 
 res.writeHead(404, 'Not Found'); 
 res.write('404: File Not Found!'); 
 return res.end(); 
 } 
 
 res.statusCode = 200; 
 
 res.write(data); 
 return res.end(); 
 }); 
 }; 
 
 var httpServer = http.createServer(staticServe); 
 
 httpServer.listen(8080); 

Большой! Теперь у нас есть примитивный статический файловый сервер.

Есть еще немало улучшений, которые мы можем внести в этот код, например, кэширование файлов, добавление дополнительных HTTP-заголовков и проверки безопасности. Мы кратко рассмотрим некоторые из них в следующих нескольких подразделах.

Кеширование

Самый простой метод кэширования - просто использовать неограниченное кэширование в памяти. Это хорошая отправная точка, но ее не следует использовать в производстве (вы не всегда можете просто кэшировать все в памяти). Все, что нам нужно здесь сделать, это создать простой объект JavaScript для хранения содержимого из файлов, которые мы ранее загрузили. Затем при последующих запросах файлов мы можем проверить, был ли файл уже загружен, используя путь к файлу в качестве ключа поиска. Если данные существуют в объекте кеша для данного ключа, мы возвращаем сохраненное содержимое, в противном случае мы открываем файл, как и раньше:

 "use strict"; 
 
 var fs = require('fs'); 
 var path = require('path'); 
 var http = require('http'); 
 
 var staticBasePath = './static'; 
 
 var cache = {}; 
 
 var staticServe = function(req, res) { 
 var resolvedBase = path.resolve(staticBasePath); 
 var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); 
 var fileLoc = path.join(resolvedBase, safeSuffix); 
 
 // Check the cache first... 
 if (cache[fileLoc] !== undefined) { 
 res.statusCode = 200; 
 
 res.write(cache[fileLoc]); 
 return res.end(); 
 } 
 
 // ...otherwise load the file 
 fs.readFile(fileLoc, function(err, data) { 
 if (err) { 
 res.writeHead(404, 'Not Found'); 
 res.write('404: File Not Found!'); 
 return res.end(); 
 } 
 
 // Save to the cache 
 cache[fileLoc] = data; 
 
 res.statusCode = 200; 
 
 res.write(data); 
 return res.end(); 
 }); 
 }; 
 
 var httpServer = http.createServer(staticServe); 
 
 httpServer.listen(8080); 

Как я уже упоминал, нам, вероятно, не следует позволять кешу быть неограниченным, иначе мы можем занять всю системную память. Лучшим подходом было бы использование более умного алгоритма кеширования, такого как lru-cache , который реализует наименее используемую концепцию кеширования. Таким образом, если файл не запрашивается какое-то время, он удаляется из кеша и сохраняется память.

Потоки

Еще одно большое улучшение, которое мы можем сделать, - это загрузка содержимого файла с использованием потоков вместо fs.readFile() . Проблема с fs.readFile() заключается в том, что ей необходимо загрузить и буферизовать все содержимое файла, прежде чем его можно будет отправить пользователю.

С другой стороны, используя поток, мы можем посылать пользователю содержимое файла по мере его загрузки с диска , побайтно. Поскольку нам не нужно ждать загрузки всего файла, это сокращает как время, необходимое для ответа на запрос пользователя, так и память, необходимую для обработки запроса, поскольку нам не нужно загружать весь файл сразу. .

Использование непотокового подхода, такого как fs.readFile() может стать для нас особенно дорогостоящим, если у пользователя медленное соединение, что означает, что нам придется хранить содержимое файла в памяти еще дольше. С потоками у нас нет этой проблемы, поскольку данные загружаются и отправляются из файловой системы только настолько быстро, насколько это возможно для соединения пользователя. Эта концепция называется противодавлением .

Здесь приведен простой пример, реализующий потоковую передачу:

 "use strict"; 
 
 var fs = require('fs'); 
 var path = require('path'); 
 var http = require('http'); 
 
 var staticBasePath = './static'; 
 
 var cache = {}; 
 
 var staticServe = function(req, res) { 
 var resolvedBase = path.resolve(staticBasePath); 
 var safeSuffix = path.normalize(req.url).replace(/^(\.\.[\/\\])+/, ''); 
 var fileLoc = path.join(resolvedBase, safeSuffix); 
 
 var stream = fs.createReadStream(fileLoc); 
 
 // Handle non-existent file 
 stream.on('error', function(error) { 
 res.writeHead(404, 'Not Found'); 
 res.write('404: File Not Found!'); 
 res.end(); 
 }); 
 
 // File exists, stream it to user 
 res.statusCode = 200; 
 stream.pipe(res); 
 }; 
 
 var httpServer = http.createServer(staticServe); 
 
 httpServer.listen(8080); 

Обратите внимание, что это не добавляет кеширования, как мы показали ранее. Если вы хотите включить его, все, что вам нужно сделать, это добавить слушателя к data потока и постепенно сохранять фрагменты в кеш. Оставлю это на ваше усмотрение :)

служить-статический

Если вам нужен статический файловый сервер для производственного использования, есть несколько других вариантов, которые вы, возможно, захотите рассмотреть, вместо того, чтобы писать свой собственный с нуля. Nginx - один из лучших вариантов, но если ваш вариант использования требует использования Node по какой-либо причине или у вас есть что-то против Nginx, то модуль serve-static также работает очень хорошо.

На мой взгляд, самое замечательное в этом модуле то, что его также можно использовать в качестве промежуточного программного обеспечения для популярного веб-фреймворка Express. Похоже, что это вариант использования для большинства людей, поскольку им в любом случае необходимо обслуживать динамический контент вместе со своими статическими файлами.

Во-первых, если вы хотите использовать его как автономный сервер, вы можете использовать модуль http serve-static и finalhandler всего в нескольких строках, например:

 var http = require('http'); 
 var finalhandler = require('finalhandler'); 
 var serveStatic = require('serve-static'); 
 
 var staticBasePath = './static'; 
 
 var serve = serveStatic(staticBasePath, {'index': false}); 
 
 var server = http.createServer(function(req, res){ 
 var done = finalhandler(req, res); 
 serve(req, res, done); 
 }) 
 
 server.listen(8080); 

В противном случае, если вы используете Express, все, что вам нужно сделать, это добавить его как промежуточное ПО:

 var express = require('express') 
 var serveStatic = require('serve-static') 
 
 var staticBasePath = './static'; 
 
 var app = express() 
 
 app.use(serveStatic(staticBasePath, {'index': false})) 
 app.listen(8080) 

Заключение

В этой статье я представил несколько вариантов запуска статического файлового сервера с Node.js. Имейте в виду, что есть еще больше вариантов, чем я упомянул здесь.

Например, есть еще несколько подобных модулей, таких как node-static и http-server . Я просто не использовал их здесь, так как serve-static гораздо более широко используется и, следовательно, вероятно, более стабилен. Просто знайте, что есть и другие варианты, которые стоит проверить.

Если у вас есть какие-либо другие улучшения, чтобы сделать статические файловые серверы быстрее, не стесняйтесь размещать их в комментариях!

comments powered by Disqus