Saturday, October 1, 2011

Force downloading through dynamic Content-Disposition

The Problem

I wanted to make it to images on my personal site could be downloaded easily. But I also want them to be normally visible in the browser. I decided to solve this with a query string parameter so:
http://epii.info/anime/2011/Fanime/Ace%20Attorney%20-%20Trucy%20Wright%208.jpg
will load the image (of Trucy from Ace Attorney), and
http://epii.info/anime/2011/Fanime/Ace%20Attorney%20-%20Trucy%20Wright%208.jpg?dl
will give the user the option to download it.

Basically I want to conditionally apply a Content-Disposition: attachment header to essentially static files, depending on the query-string.

The Solution

I found a close, but non-working example at Experts Exchange. I revised that into this:
RewriteCond %{QUERY_STRING} ^dl
RewriteRule ^((.*/)*([^/]+jpg))$ - [E=dlfilename:$3,E=fullpath:$1]
Header onsuccess set Content-Disposition "attachment;filename=\"%{dlfilename}e\"" env=dlfilename
Header onsuccess set Content-Location "/%{fullpath}e" env=dlfilename
Header onsuccess set Link "</%{fullpath}e>; rel=\"canonical\"" env=dlfilename

which is placed in an Apache 2.2 .htaccess file in the shared-hosting webserver's root.
It assumes mod_rewrite is installed and turned on (via RewriteEngine on), as well as mod_headers

Breakdown

RewriteCond %{QUERY_STRING} ^dl
The RewriteRule now only applies if the query string starts with "dl"

RewriteRule ^((.*/)*([^/]+jpg))$ - [E=dlfilename:$3,E=fullpath:$1]
This doesn't rewrite anything, but rather assigns two Apache environment variables if the file ends in jpg (and the previous RewriteCond passed). dlfilename is set with just the file name, not the full path. fullpath contains the full path (relative to .htaccess) to the requested file.

Header onsuccess set Content-Disposition "attachment;filename=\"%{dlfilename}e\"" env=dlfilename
If the dlfilename environment variable has been set, this sets the Content-Type to suggest the user saves the file, and sets the filename to the file's name. (could be problematic as implemented here if your file names contain quotes.)

Header onsuccess set Content-Location "/%{fullpath}e" env=dlfilename
This sets the Content-Location to offer a canonical location of the file. The idea is to improve image SEO by telling the search engine they aren't different. Google says they ignore this header due to it being popularly misconfigured. Just in case anyone does use it though, it's there.

Header onsuccess set Link "</%{fullpath}e>; rel=\"canonical\"" env=dlfilename
More SEO. This is Google's suggested alternative to Content-Location, though they say it only applied to Web Search for now. So if you're using images like me, this may not help you any. But if you're using PDFs, it probably will.

Saturday, July 23, 2011

FreeBSD ipfw, nat,IPsec, and IPv6 DNS

I recently setup a FreeBSD 8.3 (amd64) system as a router for a local network. The internal network is dual stack IPv4 + IPv6. This required a custom configured firewall as well beyond the standard "simple", "open", etc.

ipfw + natd

The first unexpected event happened when discovered that after rebooting, NAT no longer worked. Some investigating revealed that when processing firewall rules, when adding
${fwcmd} add 150 divert natd all from any to any via ${public_if}

during boot, it failed with the error
ipfw: getsockopt(IP_FW_ADD): Invalid Argument

Apparently I've become too expectant of relevant kernel modules to just be there. The ipdivert module wasn't being loaded until natd needed it, which is after ipfw initialized.

Solution: Add ipdivert_load="YES" to /boot/loader.conf - then it's ready well before ipfw needs it.


IPsec

In general, I've avoided using IPsec for VPNs in favor of OpenVPN or tinc. But since much of the world uses IPsec, I figured I should get more experience with it. (After this experience, I still strongly lean toward those.) I followed the guide for FreeBSD VPN over IPsec. That worked for a FreeBSD 7 system, but I ran into a non-fatal problem on my FreeBSD 8 box:
WARNING: setsockopt(UDP_ENCAP_ESPINUDP_NON_IKE): UDP_ENCAP Invalid argument

Solution:kernel config needs IPSEC_NAT_T enabled as well as IPSEC:
options IPSEC #IP security
options IPSEC_NAT_T
device crypto

(I found it surprising this wasn't all part of GENERIC by default - seems most features are, and those that aren't are in a loadable kernel module)

I fan into further problems with SA negotiation, due to not quite properly swapping EVERY IP in the config between the two machines.

I think it's worth not note to anyone reading this: make sure you test with a packet sniffer (tcpdump or wireshark or similar) - even if the secure link is not setup, packets will still transmit, just in the clear. So just because the link works doesn't mean the "private" part of the VPN is there.


DNS & IPv6 bootstrapping

More for the hell of it than any legitimate reason, I wanted to use the SixXS IPv6 DNS servers to access Google's sites via IPv6. Including from the machine working as a router. But that presented a few problems:

dhclient insists on updating resolv.conf

I need a DHCP client to get the dynamic IP for the public interface. Putting anything in /etc/resolv.conf gets overwritten. Also, IPv6 addresses are not accepted in /etc/dhclient.conf. So, I ran a nameserver on the localhost, listening on IPv4 and pointed /etc/dhclient.conf to it:
supersede domain-name-servers 127.0.0.1;

PowerDNS and IPv6

With its scary security history, and repeated new vulnerabilities, I do not run BIND. My preference has been DJBDNS tinydns/dnscache. But those lack IPv6 support. So I chose PowerDNS instead.
Doing an initial test of dig @127.0.0.1 aaaa ds.v6ns.test-ipv6.com did not work, and I discovered that unless you setup something for the outgoing IPv6 address in /usr/local/etc/pdns/recursor.conf, it won't attempt to use IPv6 while resolving DNS:
#send IPv6 queries using any source
#"::" is similar to "0.0.0.0" for IPv4
query-local-address6=::

sixxs-aiccu needs hostnames to connect

With that working, I decided to make it pass on all requests to SixXS by adding
forward-zones-recurse= .=2001:4de0:1000:a3::2, .=2001:4de0:1000:a4::2
to /usr/local/etc/pdns/recursor.conf
However on the next reboot, sixxs-aiccu wouldn't start. As I had it configured, it was connecting by hostname. So it needs working DNS before making the bridge to the IPv6 internet.

I could alter it to connect by IP, but who knows what other services may have similar problems. Since I was many interested in using it for accessing Google sites, I just changed my /usr/local/etc/pdns/recursor.conf to use:
forward-zones-recurse= google.com=2001:4de0:1000:a3::2, google.com=2001:4de0:1000:a4::2

which only uses SixXS for Google related requests, an IPv4 root servers by default, allowing it to look up the names of the servers needed to start the IPv6 tunnel through SixXS.

(Side note: youtube.com and blogger.com are CNAMEs for google.com addresses, so the AAAA lookup still goes to the SixXS servers for them, and I'm now posting here via IPv6)