Every time a Tailwind “hit piece” went viral, I quietly enjoyed the warm, fuzzy feeling of confirmation bias.
I’ve also never been the biggest advocate of anything AI (beyond providing a smarter autocomplete), but that’s a post for another day. I do, however, appreciate what companies like Vercel are doing with tools like v0, or Stackblitz with Bolt.new (or especially new startups like Lovable). They’re embarrassingly bad at generating anything close to full-stack right now, but are incredible at prototyping small drop-in React components like a copy button or even beautiful charts.
So what’s the conundrum? These tools love Tailwind (and its offspring — shadcn/ui being the poster child). And I wanted to figure out why.
As I usually do with any piece of software I want to learn more about, I used this website as a testing ground. I started with a few one-liner components, like my styled <Blockquote> for Markdown content. I quickly stumbled upon dozens of CSS to Tailwind “conversion” tools, but just as quickly realized that actually digging into Tailwind would be much easier. This lead to my first discovery:
🏆 Tailwind’s documentation is some of the best I’ve ever seen.
Seriously, it’s so good that even if you’re not using Tailwind, their docs clearly explain and visually demonstrate all of the most nonsensical CSS “features” I’ve always struggled to master.
After converting a few more components, I started to feel better and better each time I deleted a .module.css file. What I thought would make my code infinitely more complicated and messy was actually making it simpler and cleaner. Each component no longer needed its own folder. Colors and spacing were becoming more consistent. Pixels I struggled to line up previously were now coincidentally falling in line. Best of all, I didn’t have to name anything anymore! 🎉
Don’t get me wrong, I still think the syntax Tailwind forces you to write is an abomination. But honestly, so was my CSS.
Maybe that’s on me, or maybe not, but my primary reason to hate on Tailwind for years — “it makes my HTML/JSX ugly and design doesn’t belong sprinkled throughout a markup language” — just flew out the window either way. Sure, I tried to make my CSS consistent and logical, making tons of variables for colors and sizes and border radii. But that wasn’t nearly as comforting as being certain that w–12 will always be twice the width of w–6 no matter how badly I mess things up.
And on top of all of the AI tools mentioned above being Tailwind experts, the IDE support is also excellent. One click to install the official IntelliSense extension for VS Code, and suddenly everywhere I wrote text–sky–400 throughout my code had a lovely little light blue square next to it. The official Prettier extension ensures the order of class names doesn’t cause unexpected specificity problems from a rule four layers up overriding a rule you thought you were currently looking at — historically my biggest painpoint of CSS by far.
All of these tools together actually made the process of revamping this site oddly fun. It shined a spotlight on a lot of issues I had no idea were there — especially by forcing me to think “mobile-first” — and gave me an opportunity to put a new coat of paint on a design I haven’t made major changes to since my last blog post…three years ago.
So, if you’re a closeted Tailwind hater like I was, try it out. I don’t think I’ll ever love Tailwind, to be honest. But I certainly like it a lot more than I ever liked CSS.
]]>I discovered Hugo and started using it almost four years ago (6fff44e) in an effort to get back to the basics. Static site generators and buzzwordy JAMstack philosophies were all the rage, and the potential simplicity of having a website with HTML that I could have understood as a kid sounded fun and refreshing. Very quickly, I became a huge Hugo cheerleader, and I didn’t even add any JavaScript for over a year as a kind of challenge to myself. But as I saw certain JS frameworks and tools explode in popularity, some FOMO started setting in, and bringing things like React into the mix became incredibly tempting.
Turning to lighter frameworks like Lit and then Preact allowed me to tell myself I was still being a minimalist while dipping my toes in the component-filled waters. I added Preact (b755b66) to power the /projects page, the contact form, and the hit counter at the top of posts like this one. These components were made dynamic via serverless functions written in — you guessed it — JavaScript.
By the end of the Hugo chapter, I was using a cornucopia of build tools behind the scenes to keep everything glued together: Webpack, Gulp, esbuild, Babel, Terser, PostCSS, node-sass, imagemin… That’s a lot of JavaScript tooling for a site that’s allegedly avoiding JavaScript at all costs, and realizing this let me finally give myself permission to go all-in.
Enter Next.js, which caught my eye over other JS-centric SSGs (like Gatsby, 11ty, or Hexo) because it sounded like the best of both worlds. Next outputs a fully static HTML version of your site just like Hugo does and uses React to render pages dynamically (a bit like a single-page web app would behave). And once I learned how its incremental static regeneration feature works, I realized that my Hugo site was actually less static than it could be with a framework like Next! Pages like /projects, which pulls data from the GitHub API, had no actual content if you were to right-click and view source — but with Next, the project data actually exists in the static HTML initially sent by the server and is “hydrated” with the same data via React on the client-side. If you host everything on Vercel, this becomes even more seamless. (More on that later.)
devDependencies and consolidated build tooling. I don’t want to look at another Gulp task for as long as possible. Next’s built-in Webpack and Babel support has come in clutch here.152-c95759cd62a.js file you just downloaded does. Because I have no clue.Doing the actual migration was pretty boring and uneventful (besides the wealth of React knowledge I got to teach myself, which I’m sure is still less than 2% of what’s out there). I found the goal with the least mental friction was to build a 1:1 replica of the Hugo site with Next.js, to the point where a visitor wouldn’t have been able to tell there was a change, except for much faster page transition times. You can track how that went in this pull request and judge for yourself…
As always, this entire site is open source, and I’ve also archived the Hugo version in a separate repository for reference and comparison.
Let me know what you think about the “new” site as well as frameworks like React and hybrid static/server tools like Next.js. To anyone else currently using Hugo or building pure HTML sites, have you been tempted recently by a JavaScript framework du-jour? Or are you ignoring the hype and waiting for the next trendy project to come along and “change everything” yet again? (Remix is my prediction, for what it’s worth…)
]]>It is possible to use pure CSS3 media queries to do this by reading a user’s system and/or browser preference, which might be enough if you’re okay with only supporting the latest, cutting-edge browsers and OSes. But if you want your own button on your website that switches back and forth between the two modes, there’s no avoiding getting your hands a little dirty with some JavaScript.
I’ve written a simple implementation below, which…
dark-mode-toggle. For example:<button class="dark-mode-toggle">💡 Switch Themes</button>
<body>’s class between light and dark……meaning that any CSS selectors beginning with body.dark or body.light will only apply when the respective mode is active. A good place to start is by separating any color rules — your background, text, links, etc. — into a different section of your CSS. Using SASS or SCSS makes this a whole lot easier with nesting but is not required; this was written with a KISS mentality.
A very barebones example is embedded above (view the source here, or open in a new window if your browser is blocking the frame) and you can try it out on this site by clicking the 💡 lightbulb in the upper right corner of this page. You’ll notice that the dark theme sticks when refreshing this page, navigating between other pages, or if you were to return to this example weeks from now.
I have cleaned up this code a bit, added a few features, and packaged it as an 📦 NPM module (zero dependencies and still only ~500 bytes minified and gzipped!). Here’s a small snippet of the updated method for the browser (pulling the module from UNPKG), but definitely read the readme for much more detail on the API.
<button class="dark-mode-toggle" style="visibility: hidden;">
💡 Click to see the light... or not.
</button>
<script src="proxy.php?url=https%3A%2F%2Funpkg.com%2Fdark-mode-switcheroo%2Fdist%2Fdark-mode.min.js"></script>
<script>
window.darkMode.init({
toggle: document.querySelector(".dark-mode-toggle"),
classes: {
light: "light",
dark: "dark",
},
default: "light",
storageKey: "dark_mode_pref",
onInit: function (toggle) {
toggle.style.visibility = "visible"; // toggle appears now that we know JS is enabled
},
onChange: function (theme, toggle) {
console.log("Theme is now " + theme);
},
});
</script>
You can also install it straight from NPM (npm install dark-mode-switcheroo or yarn add dark-mode-switcheroo) and simply include the ESM module, which works great when bundling using Webpack, Browserify, Parcel, esbuild, etc.
import { init } from "dark-mode-switcheroo";
init({
// ...same options as browser code.
});
The example HTML and CSS below is still helpful for reference.
/*! Dark mode switcheroo | MIT License | jrvs.io/darkmode */
(function(){var e=window,t=e.document,i=t.body.classList,a=localStorage,c="dark_mode_pref",d=a.getItem(c),n="dark",o="light",r=o,s=t.querySelector(".dark-mode-toggle"),m=r===n,l=function(e){i.remove(n,o);i.add(e);m=e===n};d===n&&l(n);d===o&&l(o);if(!d){var f=function(e){return"(prefers-color-scheme: "+e+")"};e.matchMedia(f(n)).matches?l(n):e.matchMedia(f(o)).matches?l(o):l(r);e.matchMedia(f(n)).addListener((function(e){e.matches&&l(n)}));e.matchMedia(f(o)).addListener((function(e){e.matches&&l(o)}))}if(s){s.style.visibility="visible";s.addEventListener("click",(function(){if(m){l(o);a.setItem(c,o)}else{l(n);a.setItem(c,n)}}),!0)}})();
/*! Dark mode switcheroo | MIT License | jrvs.io/darkmode */
(function () {
// improve variable mangling when minifying
var win = window;
var doc = win.document;
var body = doc.body;
var classes = body.classList;
var storage = localStorage;
// check for preset `dark_mode_pref` preference in local storage
var pref_key = 'dark_mode_pref';
var pref = storage.getItem(pref_key);
// change CSS via these <body> classes:
var dark = 'dark';
var light = 'light';
// which class is <body> set to initially?
var default_theme = light;
// use an element with class `dark-mode-toggle` to trigger swap when clicked
var toggle = doc.querySelector('.dark-mode-toggle');
// keep track of current state no matter how we got there
var active = (default_theme === dark);
// receives a class name and switches <body> to it
var activateTheme = function (theme) {
classes.remove(dark, light);
classes.add(theme);
active = (theme === dark);
};
// if user already explicitly toggled in the past, restore their preference
if (pref === dark) activateTheme(dark);
if (pref === light) activateTheme(light);
// user has never clicked the button, so go by their OS preference until/if they do so
if (!pref) {
// returns media query selector syntax
var prefers = function (theme) {
return '(prefers-color-scheme: ' + theme + ')';
};
// check for OS dark/light mode preference and switch accordingly
// default to `default_theme` set above if unsupported
if (win.matchMedia(prefers(dark)).matches)
activateTheme(dark);
else if (win.matchMedia(prefers(light)).matches)
activateTheme(light);
else
activateTheme(default_theme);
// real-time switching if supported by OS/browser
win.matchMedia(prefers(dark)).addListener(function (e) { if (e.matches) activateTheme(dark); });
win.matchMedia(prefers(light)).addListener(function (e) { if (e.matches) activateTheme(light); });
}
// don't freak out if page happens not to have a toggle
if (toggle) {
// toggle re-appears now that we know user has JS enabled
toggle.style.visibility = 'visible';
// handle toggle click
toggle.addEventListener('click', function () {
// switch to the opposite theme & save preference in local storage
if (active) {
activateTheme(light);
storage.setItem(pref_key, light);
} else {
activateTheme(dark);
storage.setItem(pref_key, dark);
}
}, true);
}
})();
<!doctype html>
<html>
<head>
<style>
/* rules that apply globally */
body {
font-family: system-ui, -apple-system, sans-serif;
text-align: center;
}
a {
text-decoration: none;
}
.dark-mode-toggle {
cursor: pointer;
padding: 1em;
/* hide toggle until we're sure user has JS enabled */
visibility: hidden;
}
/* theme-specific rules -- you probably only want color-related stuff here */
/* SCSS makes this a whole lot easier by allowing nesting, but is not required */
body.light {
background-color: #fff;
color: #222;
}
body.light a {
color: #06f;
}
body.dark {
background-color: #222;
color: #fff;
}
body.dark a {
color: #fe0;
}
</style>
</head>
<body class="light">
<h1>Welcome to the dark side 🌓</h1>
<p><a href="proxy.php?url=https%3A%2F%2Fgithub.com%2Fjakejarvis%2Fdark-mode-example">View the source code.</a></p>
<button class="dark-mode-toggle">💡 Click to see the light... or not.</button>
<script src="proxy.php?url=https%3A%2F%2Fjarv.is%2Fdark-mode.min.js"></script>
</body>
</html>
When somebody pointed out the negative connotations of Git projects being created with a branch named master by default, and the possibility of this making minorities feel even more unwelcome in an industry already lacking diversity, GitHub CEO Nat Friedman quietly announced a plan to change this on Twitter (ignore the replies for your sanity).
I think many people misunderstood this tweet to mean GitHub will forcefully rename the master branch of all existing projects, which would break millions of programmers’ workflows. If anything, it’s more likely a name such as main will replace master as the default when creating a new repository, but that change hasn’t been made yet. GitLab is also discussing a similar switch to main as the default name. (Ideally, these changes would be made in tandem with the actual Git codebase, too. ~~But this doesn’t seem likely.~~)
Update: GitHub has published more details about their plan to move from
mastertomainand it will indeed be voluntary and configurable. To my surprise, the Git maintainers have also agreed to add ainit.defaultBranchsetting to customize the default branch for new repositories in Git 2.28.
But this means in the meantime, project owners are free to rename their branches as they please — and it’s pretty simple to do so, usually with minimal disruption. Some of the biggest OSS projects have already voluntarily done so. Here’s how to join them.
master branch to main:…or development, unstable, trunk, live, original; your choice!
We use branch -m to move the branch locally instead of creating a new one with checkout -b like you might be used to.
git branch -m master main
The first command is probably familiar. -u sets the new branch as the local default at the same time, and the second line ensures our local HEAD points to our new branch on GitHub.
git push -u origin main
git remote set-head origin main
You can verify this worked by running git branch -r. You should see something like origin/HEAD -> origin/main.
Setting the default branch remotely is the only step that can’t be done on the command line (although you can technically use the GitHub API). Head to Settings → Branches on GitHub to change the default branch.
master branch on GitHub:We used -m (move) to rename the master branch locally, but GitHub will still have two identical branches at this point (as you saw in the previous step). Deleting it can be a little nerve-racking, so poke around your repository on GitHub and make sure your new branch is there and the commit history is correct.
You can say good riddance to master through the GitHub UI or with this command:
git push origin --delete master
master:Do a quick search of your codebase for master to manually replace any dead references to it.
Pay attention to CI files — .travis.yml, .github/workflows/, .circleci/config.yml, etc. — and make sure there aren’t any external services relying on master being there. For example, I almost forgot to change the branch Netlify triggers auto-deploys from to build this site:
~~Unfortunately, GitHub won’t redirect links containing master to the new branch (as of now), so look for any github.com URLs as well.~~
Update: GitHub is now redirecting deleted branches to the default branch!
None of this will work on a brand new repository with zero commits. But we can hack around this limitation pretty easily…
You can create a Git alias in your local environment’s .gitconfig to make main the default branch name for new repositories. Git doesn’t let you override some native commands like git init, so we’ll create our own git new command instead (h/t @johnsyweb):
git config --global alias.new '!git init && git symbolic-ref HEAD refs/heads/main'
This may be a small gesture, but anything we can do to make even one more volunteer feel more welcome in the OSS community will always be worth the extra 10 to 15 minutes of inconvenience on my end. ✊🏾
And while we’re at it, Nat… It’s time to finally #DropICE. 🧊
Introducing the Y2K Sandbox — with fully-featured, fully-isolated, on-demand Windows Millennium Edition® virtual machines, simply to experience my first website in its natural Internet Explorer 5 habitat. And maybe play some 3D Pinball: Space Cadet. Oh, and Microsoft Bob is there too if you want to say hello and catch up. 🤓
Play in the Y2K Sandbox, at your own risk.
The backend is powered by QEMU (as a Pentium III emulator) inside isolated Docker containers, websocketd (an awesome lightweight WebSockets server written in Go), and Cloudflare Tunnels (for some protection), all tied together with some Ruby code and shell scripts. ~~I’ll push the backend scripts up to GitHub once I have a chance to untangle the spaghetti code. 🍝~~
The frontend is much simpler with a few lines of JavaScript and noVNC, a VNC-via-WebSockets library, hosted by Cloudflare Pages.
I must give credit to both charlie.bz and benjojo.co.uk, similar websites I was enamored with when they were posted on Hacker News a few years ago. Think we’ll see some websites like these with Windows 29 in a decade?
@microsoft Please don’t sue me.
Feel free to open an issue on GitHub if you run into connection glitches or have any nostalgic inspiration for software you think would be cool to install persistently on the OS image. I certainly can’t help with any actual Windows Me crashes, though — it was beyond help a long, long time ago. Like, the day it came out. But it will always have a soft spot in my heart.
Anyways… quarantine boredom is a crazy thing, am I right? 😷
]]>These are just a few incredible open source projects that didn’t exist a few months ago, but rapidly formed teams of dozens of contributors to fill both big needs and small niches in the fight to defeat the novel coronavirus, aka COVID-19.
Now that Americans are finally starting to get tested for the coronavirus, information and statistics about the results are being released state-by-state, which has led to a scattering of primary sources across the web, each releasing different figures in different forms. The COVID Tracking Project collects as much information as possible from each local health authority’s website and puts everything together in easy-to-digest tables, as well as spreadsheets and a public API.
The maintainers are also fully transparent about their process and take great care to annotate individual figures with the methodology used to arrive at each, which has earned them the trust of even the largest national news organizations reporting on COVID-19.
This one might be my favorite, simply because of its laser-like focus on solving a very specific (yet catastrophic) problem. The United States is already running out of personal protective equipment (PPE) for the healthcare professionals on the front lines of this crisis. #findthemasks.com has gathered specific donation requests and points of contact from hospitals around the country in desperate need of basic supplies.
Please look up your local hospitals on #findthemasks and follow their instructions to donate anything you have hoarded — it’s likely the single most impactful thing you can do at this point. If you don’t see your local hospital, or don’t feel comfortable shipping equipment to any hospital listed, you can also visit PPE Link and they will connect you with hospitals in your area.
I figured I’d throw in this cheeky website broadcasting a simple but serious message: STAY THE FUCK HOME!!! If you’re still not convinced of the importance of this “suggestion,” give their “Self-Quarantine Manifesto” a quick read. Now.
The GitHub community has translated the instructional essay into over a dozen different languages — including a safe-for-work version, if that helps — and they’re looking for more translators if you’re multilingual and need something besides Netflix to fill your time with while you stay the fuck home! 😉
This collection of various visualizations is fascinating (and sobering) to look at. If you’re smarter than I am and have experience in data analysis, their team (led by a GitHub engineer) would be more than happy to add your contribution to the site — they’re using Jupyter Notebooks and fastpages.
CoronaTracker is a beautiful cross-platform app for iOS and macOS with intuitive maps and charts fed by reputable live data. Apple is being justifiably picky about “non-official” Coronavirus apps in their App Store (so is Google, by the way) but you can still download the macOS app directly or compile the iOS source code yourself using Xcode if you wish.
A bit more family-friendly than #StayTheFuckHome, the Staying Home Club is maintaining a running list of over a thousand companies and universities mandating that employees and students work from home, as well as events that have been canceled or moved online. Quarantining yourself might feel lonely, but here’s solid proof that you’re far from alone right now.
This one is a bit over my head, but apparently Nextstrain is a pretty impressive open-source service targeted at genome data analysis and visualization of different pathogens. Their COVID-19 page is still awe-inspiring to look at for a layman like me, but probably a thousand times more so if you’re an actual scientist — in which case, the genome data they’ve open-sourced might be of interest to you.
Johns Hopkins University’s visual COVID-19 global dashboard has been bookmarked as my go-to source of information since the beginning of this crisis earlier this year. Now, JHU’s Center for Systems Science and Engineering has open-sourced their data and analysis for anybody to use.
COVID-19 Scenarios will probably hit everyone in a different way, depending on your levels of optimism and/or pessimism right now. It uses advanced scientific models to predict the future of the virus based on past data and future variables and assumptions you can tinker with yourself.
The maintainers at the Neher Lab in Basel, Switzerland even have a discussion thread and an open chatroom set up for both scientists and non-scientists to ask questions and post ideas, which I find really nice of them!
Similar to the COVID Tracking Project above, the Corona Data Scraper has set up an automated process to scrape verified data from across the web to form massive CSV spreadsheets and JSON objects. They even rate the quality of each source to prioritize data accordingly.
Folding@home has been around forever. I remember installing it on my family’s home computer as a curious kid and making my father infuriated over how slow it got. But they switched gears this month from using our computers to crunch various proteins and molecules in the background, and all of their power is now going towards discovering unknown “folds” in the coronavirus, which might be able to lead scientists to find better cures and potential vaccines.
You can download their software here to donate some idle computing power to their efforts — they definitely know what they’re doing by now, after pioneering en-masse distributed computing 20 years ago.
Fun fact: The team behind Folding@home has seen a huge spike in computational power this month after cryptominers started mining coronavirus proteins instead of boring, old Ethereum with their insanely overpowered GPUs! 👏
To wrap this list up, I thought I’d include yet another API fed by multiple data sources that you can use to create your own open-source project if any of these inspired you. This one is incredibly flexible in terms of query parameters and endpoints but they all return simple JSON responses like we all know and love.
Deleting 401,907 files from Dropbox… 😬
Decisions made by the top folks at Dropbox gave me an increasingly sour taste in my mouth over the past few years. The biggest red flags were:
~/Dropbox folder, which was incredibly helpful for nerds — once their main audience and biggest cheerleaders — with things like dotfiles and Git repositories.The infamous Apple Ecosystem™ has held me firmly in its grasp for over a decade now, and the main requirement of a replacement cloud storage service for me was smooth interoperability between my MacBook, iPhone, and iPad.
I’ve never been a proponent of leaving all your eggs in one basket. But it’s hard to ignore the convenience of Apple’s streamlined (and finally reliable) iCloud Drive, which is already installed on all of my devices (and actually cheaper than Dropbox gigabyte-for-gigabyte, at $9.99/month for 2 TB). In fact, it’s nearly invisible on macOS: I can simply save files in my Documents or Desktop folders as I always have and they’re uploaded in the background. Git repositories now sync just fine and my files reappeared without a hitch after I recently formatted my Mac.
I still use (and highly recommend) Backblaze (referral link) to backup my home folder and add a second layer of redundancy to storing all of my most important files on “someone else’s computer.” And as long as I remember to plug in my external SSD every so often, they’re also backed up locally via Time Machine.
There are already a few Dropbox features I’m beginning to miss, like selective sync, third-party integration, easier sharing, and an Android app (a man can dream, right?). But hopefully Apple continues to iterate on iCloud Drive, and it serves me well enough to not want to seek out another service for another ten years.
Thank you, Dropbox, for a fine relationship and for pioneering the consumer cloud storage industry. But for now, it’s just not going to work between us. 💔
]]>Pretty much all of the benefits of Netlify Analytics stem from the fact that it’s purely server-side software. This is what singularly sets it apart from Google Analytics — by far the status quo — and even self-hosted, open-source applications I’ve tried like Matomo and Fathom.
To start using Netlify Analytics, you press a few buttons on the Netlify dashboard and voilà. No need to copy and paste some obfuscated JavaScript snippet into the <head> of each page, which is a painful task for those of us who care about speed and efficiency on the web.
On top of sending yet another DNS request to one of Google’s domains — and more HTTP payloads for each outgoing click, file downloaded, etc. — Google’s analytics.js script is currently 43 KB. For a site like nytimes.com, which transfers nearly 20 MB on its homepage, this is negligible. But for simple sites like mine, which I’ve painstakingly optimized (mostly for fun, don’t judge), that doubles the size of my homepage. Matomo’s script, weighing in at 65 KB, made it even worse.
This is the big one.
In the age of GDPR (the General Data Protection Regulation in Europe), when using analytics tools and trackers without popping up a cookie consent prompt on each new visit can get you fined millions of euros, Netlify Analytics stands alone. Netlify promises its product is fully GDPR compliant. CEO Matt Biilmann explains:
“One of the things that has come out of GDPR is that a lot of large companies do intensive tracking of individual users — running scripts across a lot of different sites that capture a lot of detailed information from the browser. That puts you in the position where you have a ton of data on all kinds of people.”
And even outside of Europe, scrapping the tracking scripts on your site just makes you a courteous netizen (God, I hate that word). Not only does Google Analytics provide you with detailed information on your visitors; by default, you’re also sharing that data with Google itself, to the point where they can pinpoint your age, gender, and even your interests by cross-referencing data with your Google account and your behavior on other sites using Google Analytics.
Instead, Netlify Analytics pulls and compiles data from server logs on each of their CDN edge nodes, rather than having the visitor’s browser push data about itself back up to a third-party’s endpoint.
Netlify does store some short-term data, like IP addresses, as any normal hosting provider does. But for the purposes of analytics, the data is anonymized and only used to determine things like unique visitors vs. individual page views — and not shown to the customer. Netlify’s DPA (Data Processing Agreement) is one of the most conservative I’ve seen on the web.
Ad blocking is becoming commonplace on the World Wide Web with over 25% of users reportedly installing extensions to do so as soon as their new browser touches the net. And for good reason, since most of them also block cross-site tracking scripts like Google’s by default.
That’s a huge chunk of visitors missing that Netlify Analytics gains back for you — and probably far more if your audience is tech-savvy like those reading this post likely are. (Some might even block JavaScript completely using extensions like NoScript.)
Another tangential benefit you simply don’t get from JavaScript-based tools like Google Analytics is the “Resources Not Found” box, which separates out URLs that resulted in a 404 Not Found error. Because of the 404 tracking, I discovered how many people were still subscribed to my posts via RSS from when I used WordPress years ago, and I was able to redirect /feed and /rss to the new location.
Side note: This section has also become cluttered with requests from script kiddies who are scanning the internet for files like login.php and /wp-admin and AspCms_Config.asp (huh?) — but that’s a whole separate problem for another day.
Netlify is one of the most awesome free-as-in-beer services on the web today, providing a fast CDN and instant deployments at zero cost (up to a pretty insane amount, of course). But if you want to add Netlify Analytics, your bill suddenly jumps to $9 a month. Nine dollars! That’s over $100 per year! If you have more than 250,000 visitors per month, the cost can be even higher (to the point where you’ll need to contact Netlify’s sales team).
It makes sense that Netlify needs to subsidize the cost of providing free enterprise-grade web hosting for the rest of its non-enterprise users to stay alive. But when Google Analytics is free, this is a pretty tough ask for any hobbyist — even if Google is getting more from them than they are from Google. 😬
Clearly, as much as I wish they did, 60,000+ visitors didn’t type my website directly into the URL bar in the past month. Some of my articles have been circulating on Hacker News, Reddit, Twitter, etc. — none of which have even made a blip on the dashboard.
There are various possible reasons that referrers aren’t being sent, mostly relating to HTTP headers and increasingly sensible browser defaults, that aren’t Netlify’s fault. But this section is the most obvious example of important data you can miss out on by not tracking incoming visitors via JavaScript.
Another benefit of using Google’s own analytics service becomes glaringly apparent here: I have no idea which search terms were used to reach which page. Netlify could mitigate this a bit by separating out referrers for each individual page, though, so at least I’d know which pages were having the most organic success on search engines.
One more note: since Netlify doesn’t process IP addresses or user agents, bots crawling your site (like Googlebot and Bingbot) get counted towards your stats, possibly overinflating your ego a little more than it should.
Trying out Netlify Analytics meant switching this site from GitHub Pages to Netlify — something I still have mixed feelings about. But if I had been on Netlify the entire time, I would have gotten thirty days of historical stats backfilled right off the bat, from before I even started paying for Analytics.
Sure, this is a cool bonus. However, “thirty days” has another meaning on Netlify Analytics: it’s the absolute maximum amount of data you can access. Period, full stop. On your Analytics dashboard, you can see a window of the past month on your site — and that’s all. Day 31 is gone, seemingly forever.
I hope Netlify proves me wrong in Version 2, since analyzing trends over the course of a year (or two, or five) is an integral reason to track visitor behavior in the first place. Otherwise, it’s nearly impossible to tell which piece of content or which new feature caused your website to explode in popularity, unless you’re meticulously watching it happen in real time.
I’m super happy to see an investment in privacy-minded solutions for analytics, and the Netlify team should be proud of what they’ve built. And for the time being, I’m willing to continue forking over the nine bucks per month to give Netlify a chance to keep building upon this awesome (and, dare I say, courageous) concept. But only time will tell if others are willing to do the same — and if they are, how long they’re willing to wait before resorting back to injecting bloated JavaScript snippets and hoarding invasive amounts of our data to share with the behemoths of the internet.
Hopefully it happens within a window of 30 days, though, or else Netlify will be none the wiser! 😉
]]>Ever since President Obama injected technology into presidential politics in a historic way, one of the few bright spots of the incredibly long and exhausting race for me has been inspecting each candidate’s campaign website. They end up revealing a great deal about how much each of them is willing to invest in the internet, and how young and innovative (and potentially funny) the staff members they attract are.
More recently, though, little-known hidden Easter eggs on “404 Not Found” pages have become an outlet for the candidates’ overworked web designers to let out some humor in a sea of otherwise serious policy pages. Below are the 404 pages on each of the current candidate’s websites, along with my ranking of them — setting politics aside, for once.
I’m a huge sucker for Kate McKinnon’s spot-on impression of Warren on Saturday Night Live. And unfortunately, seeing a campaign embrace SNL is like a breath of fresh air these days. Watch all of the Kate McWarren videos so far here; you won’t regret it.
Although the designer who selected this GIF likely had thousands of choices when searching “Bernie finger wagging GIF,” the text beside it is well-written and funny — even though we both know putting a page at berniesanders.com/zxcliaosid probably won’t be a top priority of a President Sanders.
Uncle Joe has a nice and simple 404 page. I like it, along with the Ray-Bans and his choice of vanilla ice cream.
A ballsy move, considering Beto’s infamous DUI arrest in the ’90s — but still a clever ask for a donation and a great use of a GIF, even if it’s left over from his Senate campaign.
Another clean and simple page with a top-notch GIF. It injected some emotion into visiting kamalaharris.com/alskdjf.
I love, love, love Pete’s design for his whole campaign, and his beautiful 404 page is no exception. In case you didn’t know, Pete for America has an entire “Design Toolkit” publicly available for all to view and use, with really cool and in-depth explanations for all of their choices — even their color palette. Very progressive indeed.
Love the photo choice. But although pains me to go against my Senator from my home state, I still cannot stand his choice of font. Oh well, I guess that’s now a criterion for running for president in 2020.
Not sure if donating to Yang 2020 will help put a page at yang2020.com/alsdjfzoif — the actual URL I visited to grab this screenshot — but the Bitmoji Andrew looks pretty chill.
This is the 404 page of someone who won’t forget the Midwestern roots she comes from once she moves into the White House…or writes a memoir about her campaign from her Minnesota home.
I’ll never publicly say anything against a good Dad joke. This is no exception.
Another quality Dad joke here.
Yet another Dad joke? I honestly had the hardest time ranking these three.
A 404 page only a motivational author and speaker running for president could envision.
I guess this would be slightly humorous…four years ago. Time to move on from your middle-school crush, Donny.
These candidates haven’t configured a custom 404 page, settling for the default Drupal or WordPress text. Do they really think they can run the free world with their websites in this shape? 🙄 </s>
My favorite so far is my Lighthouse Audit action, which spins up a headless Google Chrome instance in an Ubuntu container and runs Google’s Lighthouse tool, which scores webpages on performance, accessibility, SEO, etc. and provides actual suggestions to improve them. It’s a perfect example of the power of combining containers with Git workflows.
The results of a Lighthouse audit on this website, after running tests in a headless Google Chrome.
It’s also been a fantastic avenue to dip my feet into the collaborative nature of GitHub and the open-source community. I’ve made some small apps in the past but these are the first projects where I’m regularly receiving new issues to help out with and impressive pull requests to merge. It’s a great feeling!
Here are the actions I’ve made so far, sorted by popularity as of this posting:
As an example of an extremely simple (and almost completely unnecessary) action, the Wait action takes one input — a unit of time — and has the pipeline sleep for that amount of time. The Dockerfile is as simple as this:
…with a super-short entrypoint.sh:
Using an action is also surprisingly simple, and more intuitive than Travis CI or CircleCI, in my humble opinion. Pipelines in GitHub Actions are called “workflows,” and live in a file with YAML syntax in .github/workflows. An example of a workflow.yml file that uses the above action to wait 10 seconds (on both pushes and pull requests) would look something like:
For a more complex example, when I forked Hugo (the static site generator used to build this website) to make some small personalized changes, I also translated their .travis.yml file into a workflow.yml file for practice, which simultaneously runs comprehensive unit tests on three operating systems (Ubuntu 18.04, Windows 10, and macOS 10.14) with the latest two Go versions each! If the tests are all successful, it builds a Docker image and pushes it to both Docker Hub and the GitHub Package Registry (also in beta).
Then another workflow, which lives in this website’s repository, pulls that Docker image, builds the Hugo site, and pushes it to GitHub Pages. All astoundingly fast. All for free.
A plethora of actions is already published on the GitHub Marketplace, with dozens more being added every week. If you are not yet in the beta, I urge you to sign up here and give it a shot. GitHub has been very receptive to feedback (so far) and I can’t wait to see GitHub Actions evolve into an enterprise-grade CI tool at the level of other competitors in this space. ❤️
]]>So, I started compiling an awesome-list of other “first code” on GitHub. It was originally aimed towards those of us who grew up in the Geocities and FrontPage and Macromedia Flash era, but coders of all ages are welcome to dust off that floppy disk or 256MB USB thumb drive (or the Wayback Machine, if you can remember your first screen name 😬) and commit your first project unmodified to GitHub for posterity — and proudly link to it on the list! (I’m trying very hard to make this a cool trend, if you couldn’t tell.)
Hopefully we can all look back at our first projects and be proud of how far we’ve come since then — no embarrassment allowed! Okay, maybe a little is fine…
Aside from my first HTML creation (circa 2001), my first real coding project was in 2003: a PHP 4 masterpiece creatively titled Jake’s Bulletin Board. I’ve published the source code in full on GitHub for your viewing pleasure and highlighted the best/worst parts below.
If you’re bored on a rainy day, potential activities could include:
Who cares if somebody wants to delete a post with the ID “*” no matter the author? (delete_reply_submit.php)
<?php
$query2 = "DELETE FROM jbb_replies
WHERE replyID ='$replyID'";
$result2 = mysql_query ($query2)
or die ($query2);
?>
Sessions based on storing an auto-incremented user ID in a cookie. (login_submit.php)
<?php
session_id($user->userID);
session_start();
$_SESSION["ck_userID"] = $user->userID;
$_SESSION["ck_username"] = $user->username;
$_SESSION["ck_groupID"] = $user->groupID;
?>
Viewing a “private” message based solely on a sequential message ID. (pm_view.php)
<?php
$query1 = "SELECT * FROM jbb_pm WHERE pmID = '$pmID'";
?>
Incredibly ambitious emoticon and BBCode support. I honestly can’t begin to explain this logic. (functions.php)
<?php
$replacement = '<img src=images/emoticons/smile.gif>';
$replacement2 = '<img src=images/emoticons/bigsmile.gif>';
$replacement3 = '<img src=images/emoticons/frown.gif>';
$replacement4 = '<img src=images/emoticons/crying.gif>';
$replacement5 = '<img src=images/emoticons/blush.gif>';
// ... yada yada yada ...
$replacement21 = '<a href="';
$replacement22 = '">';
$replacement23 = '</a>';
$replacement24 = '<FONT COLOR="';
$replacement25 = '</FONT>';
$replacement26 = '<FONT SIZE="';
$replacement27 = '<BR>';
$topicval = str_replace(':)', $replacement, $topicval);
$topicval = str_replace(':D', $replacement2, $topicval);
$topicval = str_replace(':(', $replacement3, $topicval);
$topicval = str_replace(':crying:', $replacement4, $topicval);
$topicval = str_replace(':blush:', $replacement5, $topicval);
// you get the point...
$topicval = str_replace('[URL=', $replacement21, $topicval);
$topicval = str_replace(':]', $replacement22, $topicval);
$topicval = str_replace('[/URL]', $replacement23, $topicval);
$topicval = str_replace('[FONT COLOR=', $replacement24, $topicval);
$topicval = str_replace('[/FONT]', $replacement25, $topicval);
$topicval = str_replace('[FONT SIZE=', $replacement26, $topicval);
$topicval = str_replace('
', $replacement27, $topicval);
// repeated five more times throught the code...
?>
Saving new passwords as plaintext — probably the least problematic problem. (register_submit.php)
<?php
$query = "INSERT INTO jbb_users (username, password, email, avatar) VALUES ('$username','$password','$email','images/avatars/noavatar.gif')";
?>
I guess I gave up on counting $querys by ones… (functions.php)
<?php
while ($topic = mysql_fetch_object($result30)) {
$query40 = "SELECT * FROM jbb_users WHERE userID = '$topic->userID'";
$result20 = mysql_query($query40)
or die ($query40);
$query50 = "SELECT * FROM jbb_replies WHERE replyID = '$replyID'";
$result50 = mysql_query($query50)
or die ($query50);
$reply = mysql_fetch_object($result50);
$query60 = "SELECT * FROM jbb_users WHERE userID = '$reply->userID'";
$result60 = mysql_query($query60)
or die ($query60);
$user = mysql_fetch_object($result60);
$query7 = "SELECT * FROM jbb_topics WHERE userID = '$reply->userID'";
$result7 = mysql_query($query7)
or die ($query7);
$query8 = "SELECT * FROM jbb_replies WHERE userID = '$reply->userID'";
$result8 = mysql_query($query8)
or die ($query8);
$usertopics = mysql_numrows($result7);
$userreplies = mysql_numrows($result8);
}
?>
The installation “wizard” (that’s the joke, I presume…) (sql_submit.php)
JBB Installation Wizard
And finally, JBB’s actual interface… or literally as much of it as I could get to function in 2019. (index.php)
JBB Homepage
JBB Post
]]>Most search filters require a Shodan account.
You can assume these queries only return unsecured/open instances when possible. For your own legal benefit, do not attempt to login (even with default passwords) if they aren’t! Narrow down results by adding filters like country:US or org:"Harvard University" or hostname:"nasa.gov" to the end.
The world and its devices are quickly becoming more connected through the shiny new Internet of ~~Things~~ Sh*t — and exponentially more dangerous as a result. To that end, I hope this list spreads awareness (and, quite frankly, pant-wetting fear) rather than harm.
And as always, discover and disclose responsibly! 😊
"Server: Prismview Player"
"in-tank inventory" port:10001
P372 "ANPR enabled"
mikrotik streetlight
"voter system serial" country:US
"Cisco IOS" "ADVIPSERVICESK9_LI-M"
Wiretapping mechanism outlined by Cisco in RFC 3924:
Lawful intercept is the lawfully authorized interception and monitoring of communications of an intercept subject. The term “intercept subject” […] refers to the subscriber of a telecommunications service whose communications and/or intercept related information (IRI) has been lawfully authorized to be intercepted and delivered to some agency.
"[2J[H Encartele Confidential"
http.title:"Tesla PowerPack System" http.component:"d3" -ga3ca4f2
"Server: gSOAP/2.8" "Content-Length: 583"
Shodan made a pretty sweet Ship Tracker that maps ship locations in real time, too!
"Cobham SATCOM" OR ("Sailor" "VSAT")
title:"Slocum Fleet Mission Control"
"Server: CarelDataServer" "200 Document follows"
http.title:"Nordex Control" "Windows 2000 5.0 x86" "Jetty/3.1 (JSP 1.1; Servlet 2.2; java 1.6.0_14)"
"[1m[35mWelcome on console"
Secured by default, thankfully, but these 1,700+ machines still have no business being on the internet.
"DICOM Server Response" port:104
"Server: EIG Embedded Web Server" "200 Document follows"
"Siemens, SIMATIC" port:161
"Server: Microsoft-WinCE" "Content-Length: 12581"
"HID VertX" port:4070
"log off" "select the appropriate"
"authentication disabled" "RFB 003.008"
Shodan Images is a great supplementary tool to browse screenshots, by the way! 🔎 →
The first result right now. 😞
99.99% are secured by a secondary Windows login screen.
"\x03\x00\x00\x0b\x06\xd0\x00\x00\x124\x00"
Command-line access inside Kubernetes pods and Docker containers, and real-time visualization/monitoring of the entire infrastructure.
title:"Weave Scope" http.favicon.hash:567176827
Older versions were insecure by default. Very scary.
"MongoDB Server Information" port:27017 -authentication
Like the infamous phpMyAdmin but for MongoDB.
"Set-Cookie: mongo-express=" "200 OK"
"X-Jenkins" "Set-Cookie: JSESSIONID" http.title:"Dashboard"
"Docker Containers:" port:2375
"Docker-Distribution-Api-Version: registry" "200 OK" -gitlab
"dnsmasq-pi-hole" "Recursion: enabled"
root via Telnet 🔎 →"root@" port:23 -login -password -name -Session
A tangential result of Google’s dumb fractured update approach. 🙄 More information here.
"Android Debug Bridge" "Device" port:5555
Lantronix password port:30718 -secured
"Citrix Applications:" port:1604
Vulnerable (kind of “by design,” but especially when exposed).
"smart install client active"
PBX "gateway console" -password port:23
http.title:"- Polycom" "Server: lighttpd"
Telnet Configuration: 🔎 →
"Polycom Command Shell" -failed port:23
"Server: Bomgar" "200 OK"
"Intel(R) Active Management Technology" port:623,664,16992,16993,16994,16995
HP-ILO-4 !"HP-ILO-4/2.53" !"HP-ILO-4/2.54" !"HP-ILO-4/2.55" !"HP-ILO-4/2.60" !"HP-ILO-4/2.61" !"HP-ILO-4/2.62" !"HP-iLO-4/2.70" port:1900
"x-owa-version" "IE=EmulateIE7" "Server: Microsoft-IIS/7.0"
"x-owa-version" "IE=EmulateIE7" http.favicon.hash:442749392
"X-AspNet-Version" http.title:"Outlook" -"x-owa-version"
"X-MS-Server-Fqdn"
Produces ~500,000 results…narrow down by adding “Documents” or “Videos”, etc.
"Authentication: disabled" port:445
Specifically domain controllers: 🔎 →
"Authentication: disabled" NETLOGON SYSVOL -unix port:445
Concerning default network shares of QuickBooks files: 🔎 →
"Authentication: disabled" "Shared this folder to access QuickBooks files OverNetwork" -unix port:445
"220" "230 Login successful." port:21
"Set-Cookie: iomega=" -"manage/login.html" -http.title:"Log In"
Redirecting sencha port:9000
"Server: Logitech Media Server" "200 OK"
"X-Plex-Protocol" "200 OK" port:32400
"CherryPy/5.1.0" "/home"
Example images not necessary. 🤦
"Server: yawcam" "Mime-Type: text/html"
("webcam 7" OR "webcamXP") http.component:"mootools" -401
"Server: IP Webcam Server" "200 OK"
html:"DVR_H264 ActiveX"
"Serial Number:" "Built:" "Server: HP HTTP"
ssl:"Xerox Generic Root"
"SERVER: EPSON_Linux UPnP" "200 OK"
"Server: EPSON-HTTP" "200 OK"
"Server: KS_HTTP" "200 OK"
"Server: CANON HTTP Server"
"Server: AV_Receiver" "HTTP/1.1 406"
Apple TVs, HomePods, etc.
"\x08_airplay" port:5353
"Chromecast:" port:8008
"Model: PYNG-HUB"
title:"OctoPrint" -title:"Login" http.favicon.hash:1307375944
"ETH - Total speed"
Substitute .pem with any extension or a filename like phpinfo.php.
http.title:"Index of /" http.html:".pem"
Exposed wp-config.php files containing database credentials.
http.html:"* The wp-config.php creation script uses this file"
"Minecraft Server" "protocol 340" port:25565
net:175.45.176.0/22,210.52.109.0/24,77.94.35.0/24
Port 17 (RFC 865) has a bizarre history…
port:17 product:"Windows qotd"
"X-Recruiting:"
If you’ve found any other juicy Shodan gems, whether it’s a search query or a specific example, open an issue/PR on GitHub!
Bon voyage, fellow penetrators! 😉
]]>Last month, the founder of a small startup got quite a bit of attention on Twitter (and Hacker News) when he called out DigitalOcean who, in his words, “killed” his company. Long story short: DigitalOcean’s automated abuse system flagged the startup’s account after they spun up about ten powerful droplets for some CPU-intensive jobs and deleted them shortly after — which is literally the biggest selling point of a “servers by the hour” company like DigitalOcean, by the way — and, after replying to the support ticket, an unsympathetic customer support agent declined to reactivate the account without explanation. Nicolas had no way of even accessing his data, turning the inconvenient but trivial task of migrating servers into a potentially fatal situation for his company.
Predictably, there were a lot of Monday-morning quarterbacks who weighed in, scolding him for not having backups (he did, but they were also stored on DigitalOcean) and not paying a boatload of non-existent money for expensive load balancers pointing to multiple cloud providers. Hindsight is always 20/20, of course, but if we’re talking about a small side project that exploded into a full-fledged startup with Fortune 500 clients seemingly overnight, I completely understand Nicolas’ thought process. “Let’s just take advantage of cloud computing’s #1 selling point: press a few buttons to make our servers harder, better, faster, stronger and get back to coding!”
Most of the popular one-click server providers (including DigitalOcean, as well as Linode, Vultr, and OVH) provide their own backup offerings for an additional monthly cost (usually proportional to your plan). But as Nicolas learned the hard way, any amount of backups are just more eggs in the same basket if everything is under one account with one credit card on one provider.
Luckily, crafting a DIY automated backup system using a second redundant storage provider isn’t as daunting (nor as expensive) as it might sound. The following steps are how I backup my various VPSes to a totally separate cloud in the sky.
There are quite a few tools that have been around for decades that could accomplish this task — namely rsync — but an open-source tool named Restic has won my heart for both its simplicity and the wide range of destinations it natively supports, including but not limited to:
Backups are encrypted by default, too, which is a tasty cherry on top!
Setting up Restic is certainly easier than a low-level tool like rsync, but it can still be tricky. Follow these steps and you’ll have a fully automated system making easily restorable backups of your important files in preparation for your own (likely inevitable) Cloud-pocalypse.
I host most of my projects on Linode (affiliate link) and chose Amazon’s S3 as my backup destination. S3 is easily the gold-standard in random file storage and I’d highly recommend it — unless your servers are also on Amazon with EC2, of course. My second choice would be Backblaze’s B2, which is comparable to S3 in semantics and price.
Writing steps to create an S3 bucket would be incredibly redundant, so here are Amazon’s writeups on creating one (make sure the bucket is fully private; the other default settings are fine) as well as grabbing your account’s “access keys” which will be used to authenticate Restic with S3.
Restic might be included in your OS’s default repositories (it is on Ubuntu) but it’s better to opt for the releases on Restic’s GitHub page. The binary you’ll get from apt or yum will be several versions behind, and the GitHub flavor auto-updates itself anyways.
Find the latest version of Restic on their GitHub releases page. Since I’m assuming this is a Linux server, we only want the file ending in _linux_amd64.bz2. (For a 32-bit Linux server, find _linux_386.bz2. Windows, macOS, and BSD binaries are also there.) Right-click and copy the direct URL for that file and head over to your server’s command line to download it into your home directory:
cd ~
wget https://github.com/restic/restic/releases/download/v0.9.5/restic_0.9.5_linux_amd64.bz2
Next, we’ll unzip the download in place:
bunzip2 restic_*
This should leave us with a single file: the Restic binary. In order to make Restic available system-wide and accessible with a simple restic command, we need to move it into the /usr/local/bin folder, which requires sudo access:
sudo mv restic_* /usr/local/bin/restic
sudo chmod a+x /usr/local/bin/restic
Now’s a good time to run restic to make sure we’re good to move on. If you see the version number we downloaded, you’re all set!
restic version
This step is a little different for each cloud provider. My walkthrough assumes S3, but Restic’s documentation lays out the variables you’ll need to authenticate with different providers.
If you haven’t already created a new S3 bucket and grabbed your access key and secret from the AWS console, do so now.
We need to store these keys as environment variables named AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. For now, we’ll set these temporarily until we automate everything in the next step.
export AWS_ACCESS_KEY_ID="your AWS access key"
export AWS_SECRET_ACCESS_KEY="your AWS secret"
We’ll also need to tell Restic where the bucket is located and set a secure password to encrypt the backups. You can generate a super-secure 32-character password by running openssl rand -base64 32 — just make sure you store it somewhere safe!
export RESTIC_REPOSITORY="s3:s3.amazonaws.com/your-bucket-name"
export RESTIC_PASSWORD="passw0rd123-just-kidding"
Now we’re ready to have Restic initialize the repository. This saves a config file in your S3 bucket and starts the encryption process right off the bat. You only need to run this once.
restic init
If successful, you should see a message containing created restic backend. If not, make sure you set all four environment variables correctly and try again.
Now that the hard parts are done, creating a backup (or “snapshot” in Restic terms) is as simple as a one-line command. All we need to specify is the directory you want to backup.
restic backup /srv/important/data
Running restic snapshots will list every snapshot you’ve stored. You should see one listed at this point if everything went according to plan.
All of this is fine and good, but doing this manually whenever you happen to remember to won’t be very helpful if trouble strikes. Thankfully, Linux makes it incredibly easy to automate scripts using cron jobs. So let’s set one up for this.
Make a new file at a convenient location on your server and name it backup.sh. This is where we’ll replicate everything we did above. Here’s my full .sh file:
#!/bin/bash
export AWS_ACCESS_KEY_ID="xxxxxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxxxxx"
export RESTIC_REPOSITORY="s3:s3.amazonaws.com/xxxxxxxx"
export RESTIC_PASSWORD="xxxxxxxx"
restic backup -q /srv/xxxxxxxx
(The -q flag silences the output, since we won’t be able to see it anyways.)
I highly recommend adding one final command to the end of the file: Restic’s forget feature. Constantly storing multiple snapshots a day to S3 without pruning them will rack up your bill more than you’d probably like. Using forget, we can specify how many snapshots we want to keep and from when.
This command keeps one snapshot from each of the last six hours, one snapshot from each of the last seven days, one snapshot from each of the last four weeks, and one snapshot from each of the last twelve months.
restic forget -q --prune --keep-hourly 6 --keep-daily 7 --keep-weekly 4 --keep-monthly 12
Reading the documentation for different forget options can be helpful if you want to customize these.
Save the shell script and close the editor. Don’t forget to make the script we just wrote actually executable:
chmod +x backup.sh
Lastly, we need to set the actual cron job. To do this, run sudo crontab -e and add the following line to the end:
0 * * * * /root/backup.sh
The first part specifies how often the script should run. 0 * * * * runs it right at the top of every hour. Personally, I choose to run it at the 15th minute of every other hour, so mine looks like 15 */2 * * *. crontab.guru is a nifty “calculator” to help you customize this expression to your liking — it’s definitely not the most intuitive syntax.
The second part specifies where the script we just wrote is located, of course, so set that to wherever you saved backup.sh.
Side note: In order to use restic in future shell sessions, we need to make the four environment variables permanent by adding them to your .bash_profile or .bashrc file in your home directory. Simply copy and paste the four export lines from the backup.sh file we created above to the end of either dotfile.
Take note of the next time that your new cron job should run, so we can check that it was automatically triggered. After that time — at the top of the next hour if you used my defaults in the last step — you can run restic snapshots like we did before to make sure there’s an additional snapshot listed, and optionally take the IDs of each snapshot and run restic diff ID_1 ID_2 to see what’s changed between the two.
To restore a snapshot to a certain location, grab the ID from restic snapshots and use restore like so:
restic restore 420x69abc --target ~/restored_files
You can also replace the ID with latest to restore the latest snapshot.
~~If~~ When trouble strikes and you’re setting up a new server, just re-follow steps one, two, and six and you’ll have all your files back before you know it!
There are a few other neat options for browsing and restoring snapshots, like mounting a snapshot as a disk on your file system. Read more about that on the “restoring from backup” docs page.
Again, Restic’s documentation is really, really well written, so I definitely recommend skimming it to see what else is possible.
Literally every company’s Doomsday protocols can always be improved, and external backups are just one part of redundancy. But pat yourself on the back — this might have been a convoluted process, but hopefully you’ll be able to sleep better at night knowing your startup or personal server now has a far better chance of surviving whatever the cloud rains down upon you!
If you have any questions, feel free to leave a comment or get in touch with me. Be safe out there!
]]>BERN has two features: one called “Friend-to-Friend” (described as “add everyone in your network”) and another called “Community Canvassing” (described as “talk to people around you every day, e.g. on the bus, outside the grocery store, at a park”). Both of these involve phoning home to Sanders HQ with the following information on anybody you know or meet:
But how do I know who I know, you might ask? BERN’s FAQ page helpfully answers this:
Brainstorm a list of your contacts. Think about who you have come to know over the course of your life. How do you know who you know? On a piece of paper or in a spreadsheet, write down as many names of people you know. Some ideas for brainstorming:
- Go through your phone book or, if you use Facebook, your Facebook friend list.
- Who would you invite to your birthday party or wedding? Where have you lived throughout your life? Who did you know in each of the places you have lived?
Which people can I add to my contact list the BERN app? [sic] We use the word “friend” very broadly: You can add anyone you have met and known in your life to the app.
Using either feature, a volunteer starts with a search of the database for the voter he or she is (theoretically) talking to by entering the voter’s first name, last name, and location (which only requires entering the state but can be narrowed down by town and/or ZIP code). Every match pops up with each voter’s age, sex, and location along with an option to add the information of someone who’s not listed or still needs to register to vote.
Here’s one of the instructional videos provided internally to volunteers:
…and a few privacy-related questions about the friend-to-friend feature were answered by campaign staff in a separate closed webinar for volunteers this week:
Defenders of the BERN app have pointed out that the information used is already available from public voter rolls maintained independently by each state. This is true. But these public records have never been tied to a campaign’s internal voter files through a tool that’s wide open to the entire internet, with incentives to add valuable data that benefits one candidate.
There were even unverified claims that BERN was leaking voter ID numbers, which are the same as one’s driver’s license ID numbers in some states, through JSON responses in the first few days after its release. There don’t be appear to be strict rate limits on calls to the API either, potentially inviting malicious actors from around the world — wink wink — to scrape personal data on tens of millions of Americans en masse.
BERN’s API response in Chrome DevTools
Others have noted that web-based organizing tools like BERN have been used by campaigns at all levels since President Obama’s well-oiled, futuristic machine in 2007. This is also true, and I’m a big fan of the trend they started.
But the latter category of databases — like NationBuilder and, more notably, NGP VAN’s VoteBuilder software based on the Obama campaign’s inventions and now used by almost all Democratic campaigns across the United States — are secured and strictly guarded. Volunteer accounts need to be created and approved by paid campaign organizers and are locked down to provide the bare minimum amount of information necessary for one to canvass or phone bank a shortlist of voters. Every single click is also recorded in a detailed log down to the millisecond. (This is how Bernie’s organizers got busted snooping around Hillary’s VoteBuilder data last cycle, by the way.)
NGP VAN’s audit of the Sanders campaign’s VoteBuilder activity
BERN is taking this to an unprecedented level. Allowing anybody on the internet to sign up and add others’ personal information to the campaign’s database without their knowledge is troubling, especially when you consider the gamified “points” system they’ve added as an incentive to report as much information on as many people as possible.
BERN discussion on /r/SandersForPresident thread
In addition to the points system, it was revealed in the webinar mentioned above that the campaign is planning on giving out shiny rewards based on how many friends one adds, setting expectations at 50+ contacts to reach the “Bernie Super Bundler” tier — whatever that means.
In the middle of the webinar, the organizer also paused the presentation for fifteen minutes — complete with a countdown clock — and told volunteers to race to add as many of their friends as possible in that time. She announced afterwards that participants added 20 to 40 friends into the app on average, with some allegedly adding close to 100 in fifteen minutes.
The Privacy Policy link at the bottom of the app links to a generic policy that looks like it’s been copied from a default Wix website. There’s no mention of the BERN app, no details of how they explicitly use our information, and no sign of an opt-out procedure.
Without getting too political — everyone who knows me already knows what I think of Bernie — it’s hard to refute that his “bros” are notorious for harassment and internet trolling. Giving them any additional information beyond the Twitter handles of their targets is surely not going to help detoxify the discourse this time around.
Count me out of feeling the Bern and the BERN. Just regular old heartburn for me. 🤢
]]>A recent post on Hacker News pointed out something I’ve noticed myself over the past year — the Archive.is website archiving tool (aka Archive.today and a few other TLDs) appears unresponsive when I’m on my home network, where I use Cloudflare’s fantastic public DNS service, 1.1.1.1. I didn’t connect the two variables until I read this post, where somebody noticed that the Archive.is domain resolves for Google’s 8.8.8.8 DNS, but not 1.1.1.1. An interesting and timeless debate on privacy versus convenience ensued.
Matthew Prince, the CEO and co-founder of Cloudflare (who’s also very active on Hacker News), responded to the observation with a detailed explanation of what’s happening behind the scenes, revealing that Archive.is’s owner is actively refusing to resolve their own website for 1.1.1.1 users because Cloudflare’s DNS offers too much privacy. Excerpt below, emphasis mine:
We don’t block archive.is or any other domain via 1.1.1.1. […] Archive.is’s authoritative DNS servers return bad results to 1.1.1.1 when we query them. I’ve proposed we just fix it on our end but our team, quite rightly, said that too would violate the integrity of DNS and the privacy and security promises we made to our users when we launched the service. […] The archive.is owner has explained that he returns bad results to us because we don’t pass along the EDNS subnet information. This information leaks information about a requester’s IP and, in turn, sacrifices the privacy of users. Read more »
In other words, Archive.is’s nameservers throw a hissy fit and return a bogus IP when Cloudflare doesn’t leak your geolocation info to them via the optional EDNS client subnet feature. The owner of Archive.is has plainly admitted this with a questionable claim (in my opinion) about the lack of EDNS information causing him “so many troubles.”
He’s even gone as far as replying to support requests by telling people to switch to Google’s DNS, which — surprise! — offers your location to nameservers with pleasure.
I wrote the following reply to Matthew, praising his team’s focus on the big picture:
Honestly, Cloudflare choosing not to hastily slap a band-aid on a problem like this just makes me feel more compelled to continue using 1.1.1.1.
I hesitate to compare this to Apple calling themselves “courageous” when removing the headphone jack, but in this case, I think the word is appropriate. I’ll happily stand behind you guys if you take some PR hits while forcing the rest of the industry to make DNS safer — since it is understandable, admittedly, for users to conclude that “Cloudflare is blocking websites, sound the alarms!” at first glance.
Sure, it’s annoying that I’ll need to use a VPN or change my DNS resolvers to use a pretty slick (and otherwise convenient) website archiver. But I’m more happy to see that Cloudflare is playing the privacy long-game, even at the risk of their users concluding that they’re blocking websites accessible to everyone else on the internet.
]]>If you examine my homepage long enough, you might notice the 👋 hand emoji at the top subtly waving at you. This was easily accomplished using a few lines of CSS with a feature called @keyframes — no bulky GIFs involved, and no JS mess or jQuery overkill required.
Below are the code snippets you can grab and customize to make your own “waving hand” 👋 emojis actually wave, and a CodePen playground for live testing.
.wave {
animation-name: wave-animation; /* Refers to the name of your @keyframes element below */
animation-duration: 2.5s; /* Change to speed up or slow down */
animation-iteration-count: infinite; /* Never stop waving :) */
transform-origin: 70% 70%; /* Pivot around the bottom-left palm */
display: inline-block;
}
@keyframes wave-animation {
0% { transform: rotate( 0.0deg) }
10% { transform: rotate(14.0deg) } /* The following five values can be played with to make the waving more or less extreme */
20% { transform: rotate(-8.0deg) }
30% { transform: rotate(14.0deg) }
40% { transform: rotate(-4.0deg) }
50% { transform: rotate(10.0deg) }
60% { transform: rotate( 0.0deg) } /* Reset for the last half to pause */
100% { transform: rotate( 0.0deg) }
}
<h1>Hi there! <span class="wave">👋</span></h1>
That’s it! More hands and skin tones can be found on 📕 Emojipedia.
👋🏼 Toodles!
]]>The following steps to submit a pull request will work on Git repositories hosted anywhere — on GitLab, Bitbucket, Azure DevOps, etc. — but most open-source repositories one would want to contribute to are likely on GitHub, which is what we’ll be using.
Starting from the very beginning, we’ll fork an existing repository to our account, clone the fork locally, commit your changes to a new branch, and push it back upstream to GitHub to submit for approval.
Assuming you’re using GitHub, this step is easy. Just find the repository you’re contributing to and press the Fork button in the upper right. This will create an exact copy of the repository (and all of its branches) under your own username.
GitHub will automatically redirect you to the forked repository under your username. This is the repository you need to clone to your local development environment, not the original. Grab the URL GitHub provides under the green “Clone or Download” button and plug it into the command below.
git clone [email protected]:jakejarvis/react-native.git
This step is technically optional, but important if you plan to continue contributing to a project in the future, so we might as well…
Once you’ve forked a repository, changes to the original (or “upstream”) repository are not pushed to your fork. We need to tell the new repository to follow changes made upstream to keep it fresh via remotes.
Switch directories to the forked repository you just cloned and run the following commands. Replace the last part of the first line with the original repository clone URL — similar to the how you grabbed the URL in step 2, but this isn’t the one with your username.
This links the fork back to the original repository as a remote, which we’ll name upstream, and then fetch it.
git remote add --track master upstream [email protected]:facebook/react-native.git
git fetch upstream
It’s possible to make changes directly to the master branch, but this might FUBAR things down the road for complicated reasons. It’s best to checkout a new branch for each change/improvement you want to make. Replace fix-readme-typo with a more descriptive name for your changes, like add-mobile-site or update-dependencies.
git checkout -b fix-readme-typo upstream/master
This is either the easiest part or the hardest part, depending on how you look at it. 😉 At this point, you’re isolated in the new branch you just created, and it’s safe to open whatever text editor or IDE you use and go wild.
You’re probably used to these commands. Add the files you’ve changed and commit them with a descriptive message.
git add .
git commit -m "Fix grammar mistakes in the readme file"
The one difference is the branch you’re pushing to. You likely usually push to master, but in this case, we’re pushing to the branch with the name you created in step 4.
git push -u origin fix-readme-typo
You’re now all ready to submit the improvement you’ve made to the project’s maintainers for approval. Head over to the original repositories Pull Requests tab, and you should see an automatic suggestion from GitHub to create a pull request from your new branch.
I’ll admit, I need to refer back to these notes sometimes when I’m preparing to contribute to an open-source project. It’s certainly not the most intuitive process, but at least it’s exactly the same wherever the project is located — for example, I host my own small Gitea server to back up some of my GitHub account. This instant compatibility between completely different services is precisely what makes Git great! 🏆
]]>Not only are takeovers a fun way to dip your toes into penetration testing, but they can also be incredibly lucrative thanks to bug bounty programs on services like HackerOne and Bugcrowd, where corporations pay pentesters for their discoveries.
Huge rewards for subdomain takeovers on HackerOne!
For a deep dive on the implications of takeovers, which can be a pretty serious vector of attack for malicious actors to obtain information from users of the targeted company, Patrik Hudak wrote a great post here. Definitely take some time to skim through it and come back here when you’re ready to hunt for a potential takeover yourself.
The most common services eligible for takeovers of abandoned subdomains are the following:
On my GitHub profile, you’ll find a Go-based tool named subtake (based on subjack).
This tool takes a list of CNAME records to check and outputs potential takeover candidates pointing to these services. But how in the world do we get a list of every CNAME on the internet?
Conveniently, Rapid7 publishes a monthly list for us through their Project Sonar survey!
Project Sonar is a security research project by Rapid7 that conducts internet-wide surveys across different services and protocols to gain insights into global exposure to common vulnerabilities. The data collected is available to the public in an effort to enable security research.
One of their free monthly datasets is called Forward DNS, where you’ll find .json files named xxxx-fdns_cname.json.gz. Within the subtake repository, there’s an automated script named sonar.sh, which downloads the dataset for you and outputs a simple text file of CNAMEs pointed to any of the services listed above. Once you’ve cloned the subtake repository and grabbed the timestamp part of the filename (the string that precedes -fdns_cname.json.gz), usage of the script is as follows:
./sonar.sh 2019-03-30-1553989414 sonar_output.txt
This new text file contains both active and abandoned subdomains pointing to any of the services listed above — we still need to narrow it down to the takeover candidates by attempting to actually resolve each of them, which is where subtake comes into play. To install subtake, make sure Go is installed first and run the following:
go get github.com/jakejarvis/subtake
For a detailed description of the different options you can play around with, see the full readme on GitHub — but here’s a simple example command that uses 50 threads to take the CNAMEs listed in sonar_output.txt and outputs potentially vulnerable subdomains to vulnerable.txt.
subtake -f sonar_output.txt -c fingerprints.json -t 50 -ssl -a -o vulnerable.txt
This could take quite a while — up to a day, depending on your CPU, memory, and bandwidth — so I usually run it on a VM in the cloud and use Linux’s screen command to keep it running and check in periodically. There will also be many unavoidable false positives that you’ll need to check yourself by trying to claim the abandoned name on the corresponding service’s portal, which is why I keep using the term potential takeovers.
I also have a collection of root domains of companies offering bounties through HackerOne or Bugcrowd at a different GitHub repository. Using the grep-friendly text file, it’s easy to use grep to narrow down your vulnerable.txt list even more:
grep -f grep.txt vulnerable.txt
In my view, takeovers are a fantastic way to begin a side hustle in bug bounties, simply due to the fact that once you’ve taken over a subdomain, you don’t need to worry about another hunter beating you to the punch and reporting it before you.
Since you have this luxury of time, it becomes extremely important that you let your adrenaline subside and follow responsible disclosure guidelines — especially in the creation of a “proof of concept” file with your username at an obscure location, not at index.html. I won’t go over the details of writing a report because Patrik Hudak wrote another great post about it here. This is an example of one of my own reports (company name censored because it has not been publicly disclosed) on Bugcrowd:
I have found three subdomains of ********.com vulnerable to takeovers via unclaimed endpoints at Azure’s Traffic Manager. I have claimed these endpoints and redirected them to a blank page to prevent a bad actor from doing so in the meantime, and hosted a POC file at obscure URLs. These are the following domains I discovered and the outdated endpoints on Azure to which they point:
xxxx.********.com —> aaa.trafficmanager.net
yyyy.********.com —> bbb.trafficmanager.net
zzzz.********.com —> ccc.trafficmanager.net
…and the proof-of-concept files are at the following locations:
http://xxxx.********.com/poc-d4ca9e8ceb.html
http://yyyy.********.com/poc-d4ca9e8ceb.html
http://zzzz.********.com/poc-d4ca9e8ceb.html
I have not hosted any other file nor attempted any other vector of attack. You’re probably familiar with takeovers like this by now, but through this vulnerability, it would be possible for an attacker to obtain cookies and other sensitive information from your users via phishing, cookie hijacking, or XSS. It is also possible to obtain SSL certificates for ********.com subdomains from CAs that only require domain validation such as Let’s Encrypt, but I have not attempted to do so. More info on possible attack vectors can be found here.
Please let me know when you’ve received this report and I’ll delete the endpoints from my personal Azure account, so you can either reclaim them or remove the subdomains entirely from your DNS records. Thanks!
I removed the company’s name because an important part of responsible disclosure is the disclosure, or lack thereof. Until the company explicitly gives permission to publicly disclose the vulnerability after patching it — and there are built-in features on both HackerOne and Bugcrowd to request this — it’s not okay to talk about it publicly.
The poc-d4ca9e8ceb.html proof-of-concept file contained this single, hidden line:
<!-- subdomain takeover POC by @jakejarvis on Bugcrowd -->
No self-promotional links or redirects, no examples of XSS/cookie hijacking to be “helpful” (no matter how harmless), no funny business of any kind.
I have several more improvements I want to make to subtake (like integrating the sonar.sh script into the main Go executable, polishing the all-in-one automated Docker image, a self-updating list of service fingerprints, etc.) but still feel free to make a suggestion and/or contribute to the repository in the meantime.
Happy hunting, fellow penetrators! 😉
]]>In 2019, it’s becoming more and more important to harden websites via HTTP response headers, which all modern browsers parse and enforce. Multiple standards have been introduced over the past few years to protect users from various attack vectors, including Content-Security-Policy for injection protection, Strict-Transport-Security for HTTPS enforcement, X-XSS-Protection for cross-site scripting prevention, X-Content-Type-Options to enforce correct MIME types, Referrer-Policy to limit information sent with external links, and many, many more.
Cloudflare Workers are a great feature of Cloudflare that allows you to modify responses on-the-fly between your origin server and the user, similar to AWS Lambda (but much simpler). We’ll use a Worker to add the headers.
Workers can be enabled for $5/month via the Cloudflare Dashboard. (It’s worth noting, once enabled, Workers can be used on any zone on your account, not just one website!).
If you run your own server, these can be added by way of your Apache or nginx configuration. But if you’re using a shiny static site host like GitHub Pages, Amazon S3, Surge, etc. it may be difficult or impossible to do so.
The following script can be added as a Worker and customized to your needs. Some can be extremely picky with syntax, so be sure to read the documentation carefully. You can fiddle with it in the playground, too. Simply modify the current headers to your needs, or add new ones to the newHeaders or removeHeaders arrays.
let addHeaders = {
"Content-Security-Policy": "default-src 'self'; upgrade-insecure-requests",
"Strict-Transport-Security": "max-age=1000",
"X-XSS-Protection": "1; mode=block",
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "same-origin",
};
let removeHeaders = ["Server", "Public-Key-Pins", "X-Powered-By", "X-AspNet-Version"];
addEventListener("fetch", (event) => {
event.respondWith(fetchAndApply(event.request));
});
async function fetchAndApply(request) {
// Fetch the original page from the origin
let response = await fetch(request);
// Make response headers mutable
response = new Response(response.body, response);
// Set each header in addHeaders
Object.keys(addHeaders).map(function (name, index) {
response.headers.set(name, addHeaders[name]);
});
// Delete each header in removeHeaders
removeHeaders.forEach(function (name) {
response.headers.delete(name);
});
// Return the new mutated page
return response;
}
Once you’re done, you can analyze your website’s headers and get a letter grade with Scott Helme’s awesome Security Headers tool. His free Report-URI service is another great companion tool to monitor these headers and report infractions your users run into in the wild.
You can view my website’s full Worker script here and check out the resulting A+ grade!
]]>My full dotfiles are posted at this Git repository, but here’s a summary of the ones I find most helpful that you can add to your own .bash_profile or .bashrc file.
Check your current IP address (IPv4 or IPv6 or both) — uses my ⚡ fast simpip server!
alias ip4="curl -4 simpip.com --max-time 1 --proto-default https --silent"
alias ip6="curl -6 simpip.com --max-time 1 --proto-default https --silent"
alias ip="ip4; ip6"
Check your current local IP address:
alias iplocal="ipconfig getifaddr en0"
Check, clear, set (Google DNS or Cloudflare DNS or custom), and flush your computer’s DNS, overriding your router:
alias dns-check="networksetup -setdnsservers Wi-Fi"
alias dns-clear="networksetup -getdnsservers Wi-Fi"
alias dns-set-cloudflare="dns-set 1.1.1.1 1.0.0.1"
alias dns-set-google="dns-set 8.8.8.8 8.8.4.4"
alias dns-set-custom="networksetup -setdnsservers Wi-Fi " # example: dns-set-custom 208.67.222.222 208.67.220.220
alias dns-flush="sudo killall -HUP mDNSResponder; sudo killall mDNSResponderHelper; sudo dscacheutil -flushcache"
Start a simple local web server in current directory:
alias serve="python -c 'import SimpleHTTPServer; SimpleHTTPServer.test()'"
Test your internet connection’s speed (uses 100MB of data):
alias speed="wget -O /dev/null http://cachefly.cachefly.net/100mb.test"
Query DNS records of a domain:
alias digg="dig @8.8.8.8 +nocmd any +multiline +noall +answer" # example: digg google.com
Make a new directory and change directories into it.
mkcd() {
mkdir -p -- "$1" &&
cd -P -- "$1"
}
Unhide and rehide hidden files and folders on macOS:
alias unhide="defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"
alias rehide="defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"
Force empty trash on macOS:
alias forcetrash="sudo rm -rf ~/.Trash /Volumes/*/.Trashes"
Quickly lock your screen on macOS:
alias afk="/System/Library/CoreServices/Menu\ Extras/User.menu/Contents/Resources/CGSession -suspend"
Update Homebrew packages, global NPM packages, Ruby Gems, and macOS in all one swoop:
alias update="brew update; brew upgrade; brew cleanup; npm install npm -g; npm update -g; sudo gem update --system; sudo gem update; sudo gem cleanup; sudo softwareupdate -i -a;"
Copy your public key to the clipboard:
alias pubkey="more ~/.ssh/id_rsa.pub | pbcopy | echo '=> Public key copied to pasteboard.'"
Undo the most recent commit in current Git repo:
alias gundo="git push -f origin HEAD^:master"
Un-quarantine an “unidentified developer’s” application blocked by Gatekeeper on macOS’s walled ~~prison~~ garden:
alias unq="sudo xattr -rd com.apple.quarantine"
Quickly open a Bash prompt in a running Docker container:
docker-bash() {
docker exec -ti $1 /bin/bash
}
Pull updates for all Docker images with the tag “latest”:
alias docker-latest="docker images --format '{{.Repository}}:{{.Tag}}' | grep :latest | xargs -L1 docker pull"
This odd hack is needed to run any of these aliases as sudo:
alias sudo="sudo "
View all of my dotfiles here or check out other cool programmers’ dotfiles over at this amazing collection.
]]>df -dh = WTF
VMware Workstation and Fusion normally work hard to minimize the size of virtual hard disks for optimizing the amount of storage needed on your host machine . On Windows virtual machines, VMware has a “clean up” function, which detects newly unused space and makes the size of the virtual hard disk smaller accordingly. You’ll notice that even if you create a virtual machine with a capacity of 60 GB, for example, the actual size of the VMDK file will dynamically resize to fit the usage of the guest operating system. 60 GB is simply the maximum amount of storage allowed; if your guest operating system and its files amount to 20 GB, the VMDK file will simply be 20 GB.
VMware can be set to automatically optimize and shrink virtual hard disks as you add and, more importantly, remove files — but this automatic “clean up” setting is disabled by default. Either way, cleaning up virtual machines works like a charm…when you have Windows as a guest operating system with an NTFS disk.
As a developer, I have several VMs with various Linux-based guest OSes — and, for some reason, VMware doesn’t know how to optimize these. If you poke around in VMware, you’ll find that the clean up button is greyed-out under the settings of a Linux VM.
Commonly, I’ll use a few gigabytes of storage for a project and then delete the files from the guest when I’m done. Let’s say that my Debian guest starts at 10 GB and I use 5 GB for my project, totaling 15 GB. The VMDK file will be, obviously, 15 GB. I finish the project and delete the 5 GB of its files. On a Windows guest, VMware would be able to shrink the volume back down to 10 GB — but you’ll quickly notice, annoyingly, that a Linux disk will remain at 15 GB, even though you’re no longer using that much. On a portable machine like my MacBook Air, this can be a huge waste!
The “clean up” feature that VMware has developed for Windows guests can be applied to Linux guests as well, but it’s pretty convoluted — we need to essentially clean up the VM ourselves, trick VMware to detect the free space, and manually shrink the volume.
A tiny caveat: This only works on VMs without any snapshots. Sadly, you either need to delete them or, if you care about keeping snapshots, you can backup the VM as-is to an external disk and then delete the local snapshots.
Once you’re ready, here’s how to shrink your Linux-based VM:
The open-source version of VMware Tools for Linux, open-vm-tools, has added a simple command to automate the above steps in the latest version. Make sure you have the latest update through either apt or yum, and then run the following command in the guest terminal:
vmware-toolbox-cmd disk shrink /
Thank you to commenter Susanna for pointing this out! The manual way below still works exactly the same.
Boot up your Linux virtual machine. We’ll start by optimizing the OS as much as possible before shrinking it. In addition to manually deleting files you no longer use, running this command in your terminal can free up a little more space by removing some installation caches left behind by old versions of software you’ve installed and updated:
sudo apt-get clean
This step is the crucial one. In order for VMware to detect the newly free space, we need to free it up ourselves using a little trickery. We’re going to have Linux overwrite the free space with a file full of zeros — the size of this file will be the size of however much space we’re freeing up (5 GB, in the example above) — and then delete it. These commands will create the file, wait a moment, and then delete the file:
cat /dev/zero > zero.fill
sync
sleep 1
sync
rm -f zero.fill
Depending on how much space we’re freeing, this could take a while. Let it finish or else you’ll be left with an actual, real file that will occupy a ton of space — the opposite of what we’re trying to accomplish!
The final step is to tell VMware we’ve done this, and manually trigger the clean up function that works so well on Windows VMs. You’ll do this step outside of the virtual machine, so shut it down fully and exit VMware. These directions are for macOS hosts specifically — if you’re on a Linux host, I’ll assume you are able to find the VMDK file, but here’s some help if you need.
VMware on macOS makes this a little tricky, since it packages VMs in what looks like a “.vmwarevm” file, which is actually a folder. Browse to wherever you’ve saved your virtual machines, probably somewhere in your home folder, and find the location of this “.vmwarevm” androgynous item. If you click on this folder, though, it’ll just open VMware again.
We need to right click on the .vmwarevm “file,” and select Show Package Contents to see what’s really in there. You should see the actual .VMDK file sitting there — normally we’re looking for the plain VMDK file (named Virtual Disk.vmdk by default) without a bunch of numbers after it, but if you have snapshots associated with your VM, this might not be the file we actually want. But run the command below with it anyways, and the output will tell you if you need to use a different file.
Now, we’re going to run our final command in our host terminal, so open that up. Linux installations of VMware Workstation should have a simple map to the vmware-vdiskmanager utility that you can run anywhere, but on macOS we need to tell it exactly where that’s located: in the Applications folder, where Fusion is installed.
We’re going to feed this command the exact location of the VMDK file we’re shrinking. You can either do this by typing the full path to it, or by simply dragging the VMDK file onto the terminal after typing the first part of the command (up to and including “-d”). The “-d” argument will defragment the disk.
/Applications/VMware\ Fusion.app/Contents/Library/vmware-vdiskmanager -d <path to your .VMDK file>
The final command should look something like this, with your VMDK file instead:
/Applications/VMware\ Fusion.app/Contents/Library/vmware-vdiskmanager -d /Users/jake/Documents/Virtual\ Machines/Debian9.vmwarevm/Virtual\ Disk.vmdk
If you’ve done this correctly, you’ll see it defragmenting the file, and then return “Defragmentation completed successfully.” If it returns a different error, such as “This disk is read-only in the snapshot chain,” it should tell you which disk you should actually shrink. Just run the command again with that VMDK file instead.
After the defragmentation completes, we need to finally shrink the image. We do this by running the same command as you did above, but replacing the “-d” with “-k” as follows:
/Applications/VMware\ Fusion.app/Contents/Library/vmware-vdiskmanager -k <path to the same .VMDK file>
Obviously, this is a really annoying way to perform a feature that only takes one click to execute on Windows virtual machines. I don’t recommend going through this entire process every time you delete a few random files. However, if you notice the free space on your host OS is mysteriously lower than it should be, the time this takes can be well worth it.
Let’s hope this will be integrated in VMware Tools in the near future — feel free to nudge VMware about it in the meantime!
]]>I am a 24-year-old “millennial” and I passionately support Hillary Clinton for the 45th President of the United States. Yes, we exist.
My goal here isn’t to convince every Bernie believer to jump ship and support her as passionately as I do, although I feel obligated to try. I totally understand the passion for Bernie. I smile inside every time I see a young person (like my sister) become interested in politics for the first time and become directly involved in influencing the course of their own future, no matter which candidate triggered it for them. For me, it was admittedly Senator Obama. I would, however, like to put the Democratic Party primary process back into perspective, because it’s turned into a bloodsport that isn’t helpful for anybody in the long run — not for either candidate, not for our party, and certainly not for our country.
News Flash: We aren’t in the general election right now. Not even close. We’re in the middle of our own party’s primary, where the field of opponents we are choosing from are all our friends. They’re both on our side. They both agree on an overall vision for our country. Of course as individuals we choose one who we like better than the other, and root for her or him and ideally invest some time and money to help however we can. I chose Hillary a long time ago because I feel she is, if anything, overqualified for the position. Especially during this increasingly turbulent period of foreign affairs, we can’t afford to allow an entry-level applicant to experiment with our standing in the world and learn our relationships with other nations on-the-fly.
After working for months as a fellow on Hillary’s campaign in New Hampshire leading up to the first primary in the country, I could feed you all the standard campaign talking points in my sleep: After graduating from Yale Law she went to work at the Children’s Defense Fund, not a high-paying New York law firm. She went undercover in Alabama to investigate discrimination in public schools. She got juveniles out of adult prisons. She gave 8 million children healthcare. But there’s just one thing that, for some reason, is hard for people to believe: at her core she is a good, caring, and loving person who has had only selfless intentions her entire life. I promise you.
The best birthday gift. 🎉
I had the incredible chance to meet Hillary the weekend before the New Hampshire primary. Her motorcade plowed through a quiet suburb in Manchester around noon and she hopped out to go knock on the doors of some lucky families. As neighbors started coming out of their houses to shake her hand, I couldn’t restrain myself from at least trying to get close and wave hello. (By the way, it’s amazing how casual the people in New Hampshire are about meeting presidential candidates.)
I walked up nervously and told her that it was my birthday (it was) and all I wanted was for her to win, which got her attention, and I thanked her for the spotlight she had been shining on the rampant addiction epidemic in the state. Instead of nodding her head and thanking me for my support and moving along like I assumed she would — she knew she would have my vote no matter what — she locked eyes with me and asked me how I’d been affected by the issue. It felt as though she dropped everything in her life and literally put her jam-packed schedule on pause to make sure I was okay and to learn more about some dude she just met ten seconds ago. I told her that I had fallen into the trap myself when I was younger, and that the part of her detailed plan that addresses the overprescription of narcotics by doctors could have prevented me from doing so. As my conversation with her grew longer and longer, and as she respectfully asked me more and more questions about my story, I totally forgot I was casually chatting on the sidewalk with a freaking former First Lady, Senator, and Secretary of State. I promise you again: She. Is. A. Real. Person.
“I know I have some work to do, particularly with young people, but I will repeat again what I have said this week. Even if they are not supporting me now, I support them.” »
But at the end of the day, all I ask is for you to keep in mind the stakes in this overall election. They have never been higher. Last year, the spectacle of Donald “The Donald” Trump running to be the leader of the free world was purely comical and impossible not to laugh at, from the moment he entered the race via gold-plated escalator whilst blasting Neil Young. But as this racist xenophobic pumpkin is rapidly racking up actual real-life delegates thanks to votes from the poorly educated and/or the white supremacists, the thought of him being within striking distance of the desk in the Oval Office is slowly twisting a knife into the pit of my stomach. This is real. This is the big picture. This is why we need to team up and work together in any way possible as soon as possible.
I’m aware of the street cred young Democrats collect by claiming they hated Hillary before hating Hillary was cool. Hating on HRC has gone more viral than Damn Daniel. But when you ask these young voters to explain why they think she’s a liar or untrustworthy or a criminal, they can rarely put their distaste for her into actual words — or if they do, they just vomit hashtag-ready soundbites from Fox News or The Young Turks. #Benghazi. #Emails. #ReleaseTheTranscripts. Joining in on the Republican-led attacks and stooping to their level is no way to advocate for the candidate you support. If you support Bernie for the nomination, you do that by going out and talking to others about why his policies rock, what his life story is, how your story relates to his story and his policies, etc. — not by spending your day mercilessly assassinating the character of a woman you’ve never met and a woman you might very well be voting for in eight short months, unless you’re able to stomach the idea of President Trump. During primary season, you win by focusing on the merits of your own candidate, not the flaws you see in another.
As Bill Maher (an avid Bernie supporter) said this weekend, some in our party need to “learn the difference between an imperfect friend and a deadly enemy.” I don’t agree with everything Hillary has said or done. I don’t unconditionally defend every single chapter in her public record over the past 30 years (and neither does she, by the way). I don’t think that’s possible for any voter to find in a politician. But if you identify as a Democrat, she is the farthest thing from your enemy. Plain and simple. Like you and Bernie, she wants to prevent a Republican from winning in November and reversing so much of the progress we’ve made over the past seven years on their first day in office. That is our number one goal right now. And whether it gets accomplished by a President Clinton or a President Sanders, I am 100% on board either way. Let’s stop fighting each other and start fighting together.
Update: The campaign has included my snowy New Hampshire interaction with her in one of the DNC convention videos! See a few short seconds of my joy at 1:24.
]]>