Назад | Перейти на главную страницу

Apache2 с mod_mono не отвечает с частичным содержимым, когда запрашивается диапазон

У меня есть сервер Apache2 с mod_mono на виртуальной машине с Ubuntu 16.04. Я заметил, что сервер не отвечает на запросы с Range заголовок правильно. Изначально было невозможно найти конкретное время для видеофайлов, но с помощью mod_headers и добавление Header set Accept-Ranges bytes в файл VirtualHost исправил это.

Однако он никогда не отвечает 206 Partial Content, даже когда Range запрашивается. Проблема заключается в том, что, когда пользователь решает продолжить загрузку, загрузка файла начинается снова с самого начала. На мобильных устройствах это означает, что контент невозможно смотреть, потому что ответ 200 OK отправляет видео целиком, для чего устройствам просто не хватает памяти.

На изображении ниже показана проблема. Я смотрел видео и поставил его на паузу. При повторном воспроизведении видео все видео загружается снова, что требует большей пропускной способности и памяти устройства. Это тоже вызывает поиск.

Какую конфигурацию мне нужно сделать, чтобы ответ частичного содержимого работал?

Фактическая конфигурация:

httpd.conf:

ServerName exemplo

User web_server
Group web_server

ServerRoot /home/web_server/server

IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf

IncludeOptional conf-enabled/*.conf

IncludeOptional sites-enabled/*.conf

Listen 80

<IfModule ssl_module>
    Listen 443
</IfModule>

<IfModule mod_gnutls.c>
    Listen 443
</IfModule>

PidFile apache2log/httpd.pid

ErrorLog apache2log/error.log

HostnameLookups Off

LogLevel warn

<Directory "/">
  Require all denied
  Options -Indexes
  AllowOverride None
</Directory>

<FilesMatch "^\.ht">
    Require all denied
</FilesMatch>

LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent

CheckSpelling Off
CheckCaseOnly On

mono_conf.conf (он доступен на сайтах):

<VirtualHost *:80>

  ServerName local-server-1
  ServerAdmin web-admin@local-server-1
  DocumentRoot /home/web_server/server/mono

  MonoServerPath local-server-1 "/usr/bin/mod-mono-server"

  MonoSetEnv local-server-1 MONO_IOMAP=all;MONO_OLD_RX=1

  MonoApplications local-server-1 "/:/home/web_server/server/mono"
  <Location "/">
    MonoSetServerAlias local-server-1
  </Location>

  <Location /mono>
    SetHandler mono-ctrl
    Order deny,allow
    Deny from all
    Allow from 127.0.0.1
  </Location>

  <IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript
  </IfModule>

  <Directory "/home/web_server/server/mono">
    SetHandler mono
    Header set Accept-Ranges bytes
    Allow from all
    Require all granted
  </Directory>

</VirtualHost>

<VirtualHost *:443>
  SSLEngine On
  SSLCertificateFile    "/tmp/server.crt"
  SSLCertificateKeyFile "/tmp/server.key"

  ServerName local-server-1
  ServerAdmin web-admin@local-server-1
  DocumentRoot /home/web_server/server/mono

  MonoServerPath local-server-1 "/usr/bin/mod-mono-server"

  MonoSetEnv local-server-1 MONO_IOMAP=all;MONO_OLD_RX=1

  MonoApplications local-server-1 "/:/home/web_server/server/mono"
  <Location "/">
    MonoSetServerAlias local-server-1
  </Location>

  <Location /mono>
    SetHandler mono-ctrl
    Order deny,allow
    Deny from all
    Allow from 127.0.0.1
  </Location>

  <IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript
  </IfModule>

  <Directory "/home/web_server/server/mono">
    SetHandler mono
    Header set Accept-Ranges bytes
    Allow from all
    Require all granted
  </Directory>

</VirtualHost>

Запрос браузера:

GET http://localhost/arquivos/video.webm
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0
Accept: video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5
Accept-Language: pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3
Referer: http://localhost/Default.aspx
Range: bytes=18120704-
Cookie: ASP.NET_SessionId=46ED58B6CD7745987060CDF5
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

Ответ сервера:

Date: Thu, 18 Jan 2018 04:45:55 GMT
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Thu, 18 Jan 2018 02:16:55 GMT
X-AspNet-Version: 4.0.30319
Content-Length: 26728741
Cache-Control: private
Accept-Ranges: bytes
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: video/webm

Мне удалось решить эту проблему, создав обработчик специально, чтобы проверить, есть ли у запроса диапазон, а затем дать ответ с диапазоном и необходимыми заголовками.

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

<source  src="PartialHandler.ashx?arquivos/video.webm"  />

PartialHandler.ashx содержит следующий запрос ProcessRequest:

public void ProcessRequest(HttpContext context)
        {
            string a = Uri.UnescapeDataString(context.Request.Url.Query);
            if (a.StartsWith("?", StringComparison.InvariantCultureIgnoreCase))
            {
                a = a.Substring(1);
            }

            Console.WriteLine(a);

            if (a == null || a == "")
            {
                context.Response.StatusCode = 403;
                context.Response.End();
                return;
            }

            Console.WriteLine(a);

            FileInfo aberto;
            long tam_tot;
            long tam_tot_ran;
            try
            {
                aberto = new FileInfo(HttpRuntime.AppDomainAppPath + a);
                tam_tot = aberto.Length;
                tam_tot_ran = tam_tot - 1;
                context.Response.AppendHeader("Accept-Ranges", "0-" + tam_tot_ran);
                context.Response.AppendHeader("Content-Type", MimeMapping.GetMimeMapping(a));
            }
            catch (FileNotFoundException)
            {
                context.Response.StatusCode = 404;
                context.Response.End();
                return;
            }

            string allhead = context.Request.Headers.ToString();
            if (allhead.Contains("Range=bytes"))
            {
                var pedido = context.Request.Headers.Get("Range");

                if (pedido.Contains(","))
                {
                    context.Response.StatusCode = 416;
                    context.Response.End();
                    return;
                }

                Console.WriteLine(pedido); //bytes=5-15
                long end_igual = pedido.IndexOf("=", StringComparison.InvariantCultureIgnoreCase);
                long end_traco = pedido.IndexOf("-", StringComparison.InvariantCultureIgnoreCase);
                string tam_ini_str = pedido.Substring((int)end_igual + 1, (int)end_traco - 1 - (int)end_igual);
                string tam_fin_str = pedido.Substring((int)end_traco + 1);
                long.TryParse(tam_ini_str, out long tam_ini);
                long.TryParse(tam_fin_str, out long tam_fin);

                if (tam_fin > tam_tot_ran)
                {
                    context.Response.StatusCode = 416;
                    context.Response.End();
                    return;
                }

                context.Response.StatusCode = 206;

                if (tam_fin == 0)
                {
                    context.Response.AppendHeader("Content-Length", (tam_tot - tam_ini).ToString());
                    context.Response.AppendHeader("Content-Range", "bytes " + tam_ini + "-" + tam_tot_ran + "/" + tam_tot.ToString());
                    context.Response.TransmitFile(aberto.FullName, tam_ini, tam_tot_ran);
                    context.Response.End();
                }
                else
                {
                    context.Response.AppendHeader("Content-Length", (tam_fin - tam_ini + 1).ToString());
                    context.Response.AppendHeader("Content-Range", "bytes " + tam_ini + "-" + tam_fin + "/" + tam_tot.ToString());
                    context.Response.TransmitFile(aberto.FullName, tam_ini, tam_fin);
                    context.Response.End();
                }

            }
            else
            {
                context.Response.AppendHeader("Content-Length", aberto.Length.ToString());
                context.Response.TransmitFile(aberto.FullName);
                context.Response.End();
            }
        }

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