summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author53hornet <atc@53hor.net>2021-10-22 18:39:32 -0400
committer53hornet <atc@53hor.net>2021-10-22 18:39:32 -0400
commit6f5e8042470c94cef14b94b8c4251c1cfb436d95 (patch)
treea1d5874fe64eb18c64a83302b40edfc2f693fb20
parent7ecb8930235e6e7ab35cae08d257a4dbf406fa7b (diff)
download53hor-6f5e8042470c94cef14b94b8c4251c1cfb436d95.tar.xz
53hor-6f5e8042470c94cef14b94b8c4251c1cfb436d95.zip
posted: forced downloads in browser
-rw-r--r--drafts/2021-10-15-friendship-ended-with-webdavfs-and-nextcloud-client-rclone-is-my-new-best-friend-.php6
-rw-r--r--drafts/2021-10-22-drilling-out-wide-block-cylinder-heads-for-1-2-head-bolts.php9
-rw-r--r--drafts/its-not-rust-vs-go.php28
-rw-r--r--posts/2021-10-12-altruistic-angelshark.php7
-rw-r--r--posts/2021-10-22-forced-downloads-in-the-browser-via-http.php159
5 files changed, 203 insertions, 6 deletions
diff --git a/drafts/2021-10-15-friendship-ended-with-webdavfs-and-nextcloud-client-rclone-is-my-new-best-friend-.php b/drafts/2021-10-15-friendship-ended-with-webdavfs-and-nextcloud-client-rclone-is-my-new-best-friend-.php
new file mode 100644
index 0000000..e818d9b
--- /dev/null
+++ b/drafts/2021-10-15-friendship-ended-with-webdavfs-and-nextcloud-client-rclone-is-my-new-best-friend-.php
@@ -0,0 +1,6 @@
+<?php
+$title = "Friendship Ended with webdavfs (and Nextcloud Client). Rclone Is My New Best Friend!";
+if (isset($early) && $early) {
+return;
+}
+include($_SERVER['DOCUMENT_ROOT'] . '/includes/head.php');
diff --git a/drafts/2021-10-22-drilling-out-wide-block-cylinder-heads-for-1-2-head-bolts.php b/drafts/2021-10-22-drilling-out-wide-block-cylinder-heads-for-1-2-head-bolts.php
new file mode 100644
index 0000000..1082337
--- /dev/null
+++ b/drafts/2021-10-22-drilling-out-wide-block-cylinder-heads-for-1-2-head-bolts.php
@@ -0,0 +1,9 @@
+<?php
+$title = "Drilling Out Wide Block Cylinder Heads for 1/2" Head Bolts";
+if (isset($early) && $early) {
+return;
+}
+include($_SERVER['DOCUMENT_ROOT'] . '/includes/head.php');
+?>
+
+Stepping up from 17/32 to 18/32 to 19/32. Center row is smallest, next two rows out are medium, final row is largest. Based on factory dimensions, but increased.
diff --git a/drafts/its-not-rust-vs-go.php b/drafts/its-not-rust-vs-go.php
index 1ec29a8..f423744 100644
--- a/drafts/its-not-rust-vs-go.php
+++ b/drafts/its-not-rust-vs-go.php
@@ -189,3 +189,31 @@ include($_SERVER['DOCUMENT_ROOT'] . '/includes/head.php');
<p>Everything isn't great in goland either. Gofmt and the go module structure were harsh and annoying for me to use. I don't prefer languages with rigorous whitespace requirements anyways. The opinionated standard library is fickler than I would imagine, and the drama churned up over the introduction of generics (a useful, modern programming concept) is popcorn-worthy</p>
<p>The hidden cost of Go: go might be faster to actually type in. But I spend much less time debugging in Rust. The compiler/borrow-checker tells me why I can't write to memory that's already been dropped or borrowed someplace else, and suggests what I need to paste in to fix it. These kinds of bugs take a long time to track down by stepping-through Go code. Some might not even get caught. I don't use a debugger for Rust because I rarely need to step through my code one line at a time to figure out where something became NULL</p>
+
+<pre>
+<code>
+panic: runtime error: invalid memory address or nil pointer dereference
+[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x17cf291]
+
+goroutine 52 [running]:
+github.com/rclone/rclone/cmd/mountlib.(*MountPoint).Wait.func1.1()
+ github.com/rclone/rclone/cmd/mountlib/mount.go:256 +0x51
+sync.(*Once).doSlow(0xc00056c7f0, 0xc00057be38)
+ sync/once.go:68 +0xb9
+sync.(*Once).Do(...)
+ sync/once.go:59
+github.com/rclone/rclone/cmd/mountlib.(*MountPoint).Wait.func1()
+ github.com/rclone/rclone/cmd/mountlib/mount.go:254 +0x59
+github.com/rclone/rclone/lib/atexit.Run.func1()
+ github.com/rclone/rclone/lib/atexit/atexit.go:104 +0x7f
+sync.(*Once).doSlow(0x2e19420, 0x1e4ecd0)
+ sync/once.go:68 +0xb9
+sync.(*Once).Do(...)
+ sync/once.go:59
+github.com/rclone/rclone/lib/atexit.Run()
+ github.com/rclone/rclone/lib/atexit/atexit.go:102 +0xac
+github.com/rclone/rclone/lib/atexit.Register.func1.1()
+ github.com/rclone/rclone/lib/atexit/atexit.go:52 +0xf0
+created by github.com/rclone/rclone/lib/atexit.Register.func1
+ github.com/rclone/rclone/lib/atexit/atexit.go:44 +0xa5
+</code></pre>
diff --git a/posts/2021-10-12-altruistic-angelshark.php b/posts/2021-10-12-altruistic-angelshark.php
index b0602ef..b516d4b 100644
--- a/posts/2021-10-12-altruistic-angelshark.php
+++ b/posts/2021-10-12-altruistic-angelshark.php
@@ -118,10 +118,5 @@ include($_SERVER['DOCUMENT_ROOT'] . '/includes/head.php');
</p>
<p>
- I'm pretty proud of this project. It's not a very large project, but that's
- one of the things I'm proud of. It went through a couple of iterations, and
- with each I actually ended up removing code that wasn't being used to solve the
- primary problem. It's the first time I've gotten the chance to release an
- internal project as FOSS and I'm super stoked. Hopefully someone else will
- benefit from its release. Maybe I'll delve into its inner workings in another post sometime.
+ I'm pretty proud of this project. It's not a very large project, but that's one of the things I'm proud of. It went through a couple of iterations, and with each I actually ended up removing code that wasn't being used to solve the primary problem. It's the first time I've gotten the chance to release an internal project as FOSS and I'm super stoked. Hopefully someone else will benefit from its release. Maybe I'll delve into its inner workings in another post sometime.
</p>
diff --git a/posts/2021-10-22-forced-downloads-in-the-browser-via-http.php b/posts/2021-10-22-forced-downloads-in-the-browser-via-http.php
new file mode 100644
index 0000000..958a2b1
--- /dev/null
+++ b/posts/2021-10-22-forced-downloads-in-the-browser-via-http.php
@@ -0,0 +1,159 @@
+<?php
+$title = "Forced Downloads in the Browser Via HTTP";
+if (isset($early) && $early) {
+ return;
+}
+include($_SERVER['DOCUMENT_ROOT'] . '/includes/head.php');
+?>
+
+<p class="description">
+ Recently a coworker was tasked with adding a "file download" link to a web page. The file in question was being generated dynamically by the server, but we really wanted to make sure the file didn't get rendered in the user's tab. Here's how you can leverage HTTP headers to do that (and some alternative ways you shouldn't!)
+</p>
+
+<p>
+ Let's say you want to genreate a file server-side and send it back to the client's browser to download. If it's plain text, the browser will usually try to render it in a tab, without prompting the user what they want to do with it. The user could then save the page with <code>^s</code> but that's less than ideal. Ideally, they would click a link/submit a form, and immediately be prompted to save the file to save the file to disk and even open it in a local application. If they elect to always perform that action with files of that kind, the download will start immediately.
+</p>
+
+<h2>The Default: Rendering in a Tab</h2>
+
+<p>
+ As a baseline, here is some sample PHP for writing text to the client.
+</p>
+
+<pre>
+<code>
+&lt;?php
+header('content-type: text/plain');
+print("hello, world\n");
+</code>
+</pre>
+
+<p>
+ This code responds to all requests with a single header specifying the default plain text content type and then writes <code>hello, world</code> as the response body. Note that since <code>text/plain</code> is the default type, it can usually be omitted. I'm including it here for illustrative purposes.
+</p>
+
+<p>
+ You can listen for requests with this script using <code>php(1)</code> and send requests with <code>curl(1)</code>.
+</p>
+
+<pre>
+<code>
+$ nohup php -S localhost:8080 &
+[1] 47647
+appending output to nohup.out
+$ curl -v localhost:8080
+* Trying 127.0.0.1:8080...
+* Connected to localhost (127.0.0.1) port 8080 (#0)
+> GET / HTTP/1.1
+> Host: localhost:8080
+> User-Agent: curl/7.79.1
+> Accept: */*
+>
+* Mark bundle as not supporting multiuse
+< HTTP/1.1 200 OK
+< Host: localhost:8080
+< Date: Fri, 22 Oct 2021 21:39:37 GMT
+< Connection: close
+< X-Powered-By: PHP/7.4.24
+< Content-type: text/plain;charset=UTF-8
+<
+hello, world
+* Closing connection 0
+</code>
+</pre>
+
+<p>
+ Awesome. In the browser, the text gets rendered in the active tab.
+ <img src="https://nextcloud.53hor.net/index.php/s/6NCf9cXayReAkwT/preview" />
+</p>
+
+
+<h2><code>Content-Disposition</code> to the Rescue: Forcing a File Download Prompt</h2>
+
+<p>
+ We can include a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition"><code>Content-Disposition</code></a> header with the request to prompt for file download on the browser side. Here's some updated PHP:
+</p>
+
+<pre>
+<code>
+&lt;?php
+header('content-type: text/plain');
+header('content-disposition: attachment; filename=hello.txt');
+print("hello, world\n");
+</code>
+</pre>
+
+<p>
+ With <code>curl(1)</code>, we can see the header even though it just prints the text as usual.
+</p>
+
+<pre>
+<code>
+$ curl -v localhost:8080
+* Trying 127.0.0.1:8080...
+* Connected to localhost (127.0.0.1) port 8080 (#0)
+> GET / HTTP/1.1
+> Host: localhost:8080
+> User-Agent: curl/7.79.1
+> Accept: */*
+>
+* Mark bundle as not supporting multiuse
+< HTTP/1.1 200 OK
+< Host: localhost:8080
+< Date: Fri, 22 Oct 2021 21:59:47 GMT
+< Connection: close
+< X-Powered-By: PHP/7.4.24
+< Content-type: text/plain;charset=UTF-8
+< content-disposition: attachment; filename=hello.txt
+<
+hello, world
+* Closing connection 0
+</code>
+</pre>
+
+<p>
+ And on the browser side, the usesr is prompted to download <code>hello.txt</code>. The browser completely skips rendering the file in the tab (this isn't even a presented option!). Our MIME type (Text Document) is specified, and the user can optionally change their preference for what should be done with that type by default.
+ <img src="https://nextcloud.53hor.net/index.php/s/Nr67rHmQRWy48E9/preview" />
+</p>
+
+
+<p>
+ This is all it took to satisfy our primary use case. Now let's look at one of the dubious workarounds out there.
+</p>
+
+<h2>Please Don't Falsify Your <code>Content-Type</code></h2>
+
+<p>
+ This is a strange workaround that you may find elsewhere online (<a href="https://www.php.net/manual/en/function.header.php"><em>cough cough</em></a>). It looks something like this:
+</p>
+
+<pre>
+<code>
+&lt;?php
+header('content-type: application/my-random-fake-mime');
+print("hello, world\n");
+</code>
+</pre>
+
+<p>
+ What this does is pretend that your content is an unknown type to the browser. This way it doesn't attempt to render the content and instead asks the user what to do with it. Not only is this a hack-y workaround, it's also broken. First of all, this MIME doesn't exist in the specification. So it's unknown, undocumented, and unnecessary.
+</p>
+
+<p>
+ The second way this is broken is it will probably bypass any of those user-defined settings that determine what to do with files of a plain text type. If the user wanted all text files to instantly download and open in <code>NOTEPAD.EXE&trade;</code>, this would bypass that. Now the user has to add a rule for files of type <code>my-random-fake-mime</code> just for you.
+</p>
+
+<p>
+ The third (and last I'm going to talk about) way this is broken is it does not give the user a useful filename. They don't even get a file extension that they can do anything useful with. They'll have to guess by inspection, which makes for a poor user experience. See the screenshot for what this abomination looks like.
+ <img src="https://nextcloud.53hor.net/index.php/s/opmEy79ArJJ9se8/preview" />
+</p>
+
+<h2>Wrap-Up</h2>
+
+<p>
+ It's pretty easy once you know what to do. I'll quickly mention another hack online is to send multiple <code>Content-Type</code> headers to the client to "trick" the browser into downloading the file but with the correct MIME. Not only does this not work the way you would expect, it's also unecessary and goes against the HTTP spec., where only one <code>Content-Type</code> is expected with the response. If you <em>really</em> want to prove that, send as many headers with the content type as you want from the server and see how many of them <code>curl(1)</code> or the browser keeps around (hint: it's only the last one).
+</p>
+
+<p>
+ I should also mention this functionality is universal and does not apply only to PHP.
+</p>