GrabDuck

murze.be - A blog on modern PHP and Laravel

:

Recently my company Spatie launched https://dnsrecords.io, a beautiful site to quickly lookup dns records.

True to form, we also opensourced it, here is the sourcecode on GitHub. If you want to do some dns lookups in your own app, you’ll be happy to know that we extracted the dns lookup functionalities to a package.

In this blog post I’d like to share why and how we’ve built all this.

Why create another dns lookup service?

A few weeks ago Jef, our project manager at Spatie, was asked by a client to give some dns related info. Because Jef is not a technical person. He has a fear/hate relation with the terminal. So he always delegates technical questions like those to his teammates. They just use dig to quickly get dns records. Wouldn’t it be great if Jef could do the dns lookups on his own? An idea was born.

But aren’t there already many services to perform dns lookups? Let’s [Google around]
(https://www.google.be/search?q=dns+records+lookup&oq=dns+records+lookup). Here are a few of the first hits:

Most of the services work, but they are really really ugly. We couldn’t find any dns lookup service that looks beautiful. So we went ahead with creating a webapp of our own.

Introducing dnsrecords.io

My colleague Willem did an excellent job in making dnsrecords.io look beautiful. This is what you see when visiting the site.

No distractions like on the other sites. Just enter a domain to get some records. It couldn’t be simpler.

The results are displayed on a link which has a sharable link. You can just visit https://dnsrecords.io/facebook.com to get the dns records of Facebook.

If you type help you see some extra commands you can execute on our app:

Our real killer feature is of course that you can play Doom. Go on and waste some hours with this excellent game. When you’re done with that go on and drag that bookmarklet to your toolbar to lookup the dns records of the sites you visit.

Behind the scenes

We’ve open sourced the entire site. You can view the code that’s actually being deployed to our server in this repo on GitHub.

Let’s walk a bit through the code. When looking at an early version of the only controller in this project, you’ll see that everything happened inside that single controller. But because we want to easily add more commands in the features we refactored it quite a bit. In the current version the controller is quite skinny:

<?php

namespace App\Http\Controllers;

use App\Services\Commands\CommandChain;
use Illuminate\Http\Request;

class HomeController extends Controller
{
    public function index()
    {
        return view('home.index');
    }

    public function submit($command = null, Request $request)
    {
        $command = $request['command'] ?? $command;

        if (!$command) {
            return $this->index();
        }

        return (new CommandChain())->perform(strtolower($command));
    }
}

Every submitted $command is delegated to a CommandChain. Let’s take a look at the code of that CommandChain.


class CommandChain { protected $commands = [ Manual::class, Localhost::class, Clear::class, Ip::class, Doom::class, DnsLookup::class, ]; public function perform(string $command): Response { return collect($this->commands) ->map(function (string $commandClassName) { return new $commandClassName; }) ->first->canPerform($command) ->perform($command); } }

You’ll see above that we register some command classes to the chain. In perform we’ll instanciate them. The chain will ask each class: “can you perform this $command? “. The first one that can will actually perform that $command.

Let’s take a look at a such a command class. Here’s the code of the Doom command:

namespace App\Services\Commands\Commands;

use App\Services\Commands\Command;
use Symfony\Component\HttpFoundation\Response;

class Doom implements Command
{
    public function canPerform(string $command): bool
    {
        return $command === 'doom';
    }

    public function perform(string $command): Response
    {
        return redirect('https://js-dos.com/games/doom.exe.html');
    }
}

The perform function of a Command class always returns are Illuminate\Http\Response. In case of the Doom command we’ll just return a redirect to a site where you can play Doom.

Let’s take a look at another command, the DnsLookup command:

namespace App\Services\Commands\Commands;

use App\Services\Commands\Command;
use App\Services\DnsRecordsRetriever;
use Spatie\Dns\Dns;
use Symfony\Component\HttpFoundation\Response;

class DnsLookup implements Command
{
    public function canPerform(string $command): bool
    {
        return true;
    }

    public function perform(string $command): Response
    {
        $dns = new Dns($command);

        $dnsRecords = $dns->getRecords();

        $domain = $dns->getDomain($command);

        if ($dnsRecords === '') {
            $errorText = __('errors.noDnsRecordsFound', compact('domain'));

            flash()->error($errorText);

            return redirect('/');
        }

        return response()->view('home.index', ['output' => $dnsRecords, 'domain' => $domain ]);
    }
}

Noticed that canPerform returns true. This command basically says, I can handle everything. If you look again $commands array in the CommandChain you’ll see that DnsLookup is registered last. So when no other Command can handle the $command the DnsLookup will do its thing.

The real magic of looking up dns records happens inside that Spatie\Dns\Dns object which is part of our spatie/dns package.

Here’s how you can use it:

$dns = new Spatie\Dns('spatie.be');

$dns->getRecords(); // returns all records

$dns->getRecords('A'); // returns only A records
$dns->getRecords('MX'); // returns only MX records

$dns->getRecords('A', 'MX'); // returns both A and MX records
$dns->getRecords(['A', 'MX']); // returns both A and MX records

The actual lookup of dns records inside that package is being done by calling dig, a command line tool to lookup dns related info.

Here is the relevant function inside the Spatie\Dns\Dns class where that call happens.

protected function getRecordsOfType(string $type): string
{
    $command = 'dig +nocmd '.escapeshellarg($this->domain)." {$type} +multiline +noall +answer";

    $process = new Process($command);

    $process->run();

    if (! $process->isSuccessful()) {
        throw new Exception('Dns records could not be fetched');
    }

    return $process->getOutput();
}

I hope you’ve enjoyed this little behind the scenes of https://dnsrecords.io. I’d like to emphasise that creating this service was a team effort. Every member of our team helped with making the code better. We also got some great contributions from the community for which we are grateful.

This is not the first project that we’ve open sourced. If you like to see some more work by our team, take a look at our Dashboard, or the many Laravel, PHP and JavaScript packages we created previously. Want to support our open source efforts? Then consider, becoming a patreon.