1

I have a Apache2 server with mod_mono in a Virtual Machine which has Ubuntu 16.04. I have noticed that the server is not answering to requests with the Range header correctly. Initially it was not possible to even seek a specific time on video files, but using mod_headers and adding Header set Accept-Ranges bytes to the VirtualHost file corrected this.

However it never answers with 206 Partial Content, even when a Range is requested. The problem this causes is that when the user chooses to continue a download, the download of the file starts again from the beginning. On mobile devices it means the content is impossible to watch because the answer 200 OK sends the entire video, which the devices simply don't have enough memory for.

The image below shows the problem. I was watching a video and paused it. When selected to play the video again, the entire video is downloaded again, which uses more bandwidth and device's memory. Seeking causes this too.

What configuration should I do to make the Partial Content response work?

problem

The actual configuration:

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 (it's on sites-enabled):

<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>

Browser request:

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

Server response:

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
JonathanDavidArndt
  • 1,424
  • 3
  • 20
  • 29
FurretUber
  • 33
  • 2
  • have you tried to do the same without mod_mono to see if you still have the same issue? Sidenote: Also if this is 2.4 do not mix 2.2 auth directives with 2.4 ones. That is remove Order/Deny/Allow and use only Require. – Daniel Ferradal Jan 18 '18 at 07:45
  • Disabling mod_mono on the VirtualHost by commenting the line `SetHandler mono` and making a request with the Range header made the server respond with [206, as intended](https://pastebin.com/KUfJiMaY). So is the mod_mono which is causing this? – FurretUber Jan 18 '18 at 13:49
  • yes that's where I was trying to get you, I generally don't trust third party modules with a few exceptions. At least now you know what to do if you can live without that module or how to approach this issue if you need to debug or ask further. – Daniel Ferradal Jan 18 '18 at 13:55

1 Answers1

0

I was able to solve this issue by creating a handler specifically to check if the Request has a Range and then give a Response with the range and the needed headers.

On the HTML it was required to add the following on each address of the files I want to allow Range requests:

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

The PartialHandler.ashx contains the following 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();
            }
        }

With this, all videos worked on all devices I tested, the entire re-downloads of the entire video files every time it was paused or seek are no longer happening and the mobile devices are being able to play them.

FurretUber
  • 33
  • 2