Making WHOIS requests for .it domains with Angular and Go

Making WHOIS requests for .it domains with Angular and Go

In this article we will see how to create a web application to perform WHOIS queries to the .it domain name registry using Angular and Go.

In this article we will see how to create a web application to perform WHOIS queries to the .it domain name registry using Angular as the frontend framework and Go as the solution to implement the backend APIs.

Anatomy of a WHOIS request

  1. You make a TCP connection to the WHOIS server of the Italian registry using whois.nic.it as host and 43 as remote port number.
  2. The name of the .it domain is sent to the remote server followed by a line break (\r\n characters).
  3. You receive the raw response, with blocks of information separated by a line break (\n character).

Create APIs with Go

We will use the Gin framework to create REST APIs in Go. Before implementing the referral route, however, we need to separate the WHOIS request logic from the routing by defining the utils package with the necessary functions.

package utils

// utils/utils.go

import (
     "net"
     "regexp"
)

type WhoisResponse struct {
     Result string `json:"result"`
}

func ValidateDomainExtension(domain string) bool {
     m := regexp.MustCompile(`(?i)\.it$`)
     return m.MatchString(domain)
}

func WhoisRequest(domain, server string) WhoisResponse {
     conn, err := net.Dial("tcp", server+":43")
     if err != nil {
         return WhoisResponse{Result: "Error"}
     }
     defer conn.Close()
     conn.Write([]byte(domain + "\r\n"))
     buf := make([]byte, 1024)
     n, _ := conn.Read(buf)
     return WhoisResponse{Result: string(buf[:n])}
}

The main function carries out the WHOIS request, returning the struct with the result which will then be returned as JSON in the route that we will define in the routes module.

package routes

// routes/routes.go

import (
     "your-namespace/your-app-name/utils"

     "github.com/gin-gonic/gin"
)

const (
     whoisServer = "whois.nic.it"
)

func HandleWhoisRequest(c *gin.Context) {
     domain := c.Param("domain")
     if !utils.ValidateDomainExtension(domain) {
         c.JSON(400, gin.H{"error": "Invalid domain extension"})
         return
     }
     whoisResponse := utils.WhoisRequest(domain, whoisServer)
     c.JSON(200, whoisResponse)
}

The function that manages the endpoint validates the domain name passed as a route parameter, which must have the .it extension, and then returns the result in JSON format.

In the main file of our application we will enable CORS for API requests and initialize the routes and the app itself.

package main

import (
     "your-namespace/your-app-name/routes"
     "time"

     "github.com/gin-contrib/cors"
     "github.com/gin-gonic/gin"
)

const (
     appPort = ":4000"
)

func main() {

     r := gin.Default()

     config := cors.DefaultConfig()
     config.AllowAllOrigins = true
     config.AllowMethods = []string{"POST", "GET", "PUT", "OPTIONS"}
     config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization", "Accept", "User-Agent", "Cache-Control", "Pragma"}
     config.ExposeHeaders = []string{"Content-Length"}
     config.AllowCredentials = true
     config.MaxAge = 12 * time.Hour

     r.Use(cors.New(config))

     api := r.Group("/api")

     api.GET("/whois/:domain", routes.HandleWhoisRequest)

     r.Run(appPort)
}

Create the frontend with Angular

To begin, we need to create an environment variable containing the reference URL of our API in Go.

// src/environments/environment.development.ts

export const environment = {
   production: false,
   apiUrl: 'http://localhost:4000/api'
};

Now we need to define a DTO to handle the API response format, which can contain a result property or an error property. To avoid having to do a type check every time, we define both:

// src/app/core/dto/whois-response.dto.ts

export interface WhoisResponseDto {
   result: string;
   error: string;
}

The response will be in raw format which will be parsed by the dedicated service to obtain the final structure that we will define in the dedicated model.


// src/app/core/models/whois-response.model.ts

export interface WhoisResponseModel {
     domain: string;
     status: string;
     created: string;
     updated: string;
     expires: string;
     registrar: {
       organization: string;
       name: string;
       web: string;
     }
}

Now we can define the main service:

// src/app/core/services/whois.service.ts

mport { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { WhoisResponseDto } from "../dto/whois-response.dto";
import { environment } from "../../../environments/environment";
import { WhoisResponseModel } from "../models/whois-response.model";

@Injectable({
   providedIn: 'root'
})
export class WhoisService {

   private readonly apiUrl = environment.apiUrl;
   constructor(private http : HttpClient) { }

   public parseWhoisData(whoisResult: string): WhoisResponseModel {
       const lines = whoisResult.split('\n');
       const response : WhoisResponseModel = {
         domain: '',
         status: '',
         created: '',
         updated: '',
         expires: '',
         registrar: {
           organization: '',
           name: '',
           web: ''
         }
       };
       for(let i = 0; i < lines.length; i++) {
         let line = lines[i];
         if (line.includes('Domain:')) {
           response.domain = line.split(':')[1].trim();
         }
         if (line.includes('Status:')) {
           response.status = line.split(':')[1].trim();
         }
         if (line.includes('Created:')) {
           response.created = line.replace(/\d{2}:\d{2}:\d{2}/g, '').split(':')[1].trim();
         }
         if (line.includes('Last Update:')) {
           response.updated = line.replace(/\d{2}:\d{2}:\d{2}/g, '').split(':')[1].trim();
         }
         if (line.includes('Expire Date:')) {
           response.expires = line.split(':')[1].trim();
         }
         if (/Registrant|Admin|Contacts|Registrar/gi.test(line)) {
           continuous;
         }
         if(line.includes('Organization:')) {
           response.registrar.name = line.split(':')[1].trim();
         }
         if(line.includes('Name:')) {
           response.registrar.organization = line.split(':')[1].trim();
         }
         if(line.includes('Web:')) {
           response.registrar.web = line.substring(line.indexOf(':') + 1).trim();
         }
       }
       return response;
   }

   public getWhoisData(domain: string): Observable<WhoisResponseDto> {
     return this.http.get<WhoisResponseDto>(`${this.apiUrl}/whois/${domain}`);
   }
}

As you can see, the most laborious method is the one that parses the textual result obtained from the WHOIS request. To do this, we need to proceed line by line, identifying the sections that interest us and mapping them into the reference model.

We also want the user to be able to have a history of their requests. To do this we will use web storage by saving a JSON string with information about the various domains. We therefore define a model for this type of data:

// src/app/core/models/whois-storage.model.ts

import {WhoisResponseModel} from "./whois-response.model";

export interface WhoisStorageModel {
   domains: WhoisResponseModel[];
}

So let's define the reference service for managing web storage:

// src/app/core/services/storage.service.ts

import { Injectable } from '@angular/core';
import { WhoisStorageModel } from "../models/whois-storage.model";
import { WhoisResponseModel } from "../models/whois-response.model";

@Injectable({
   providedIn: 'root'
})
export class StorageService {
   constructor() { }

   addDomainToStorage(domain: string, whoisResponse: WhoisResponseModel) {
     if(this.isDomainInStorage(domain)) {
       return;
     }
     const domains = this.getDomainsFromStorage();
     domains.domains.push(whoisResponse);
     localStorage.setItem('domains', JSON.stringify(domains));
   }

   isDomainInStorage(domain: string): boolean {
     const domains = this.getDomainsFromStorage();
     return domains.domains.some(d => d.domain === domain);
   }

   removeDomainFromStorage(domain: string) {
     if(!this.isDomainInStorage(domain)) {
       return;
     }
     const domains = this.getDomainsFromStorage();
     domains.domains = domains.domains.filter(d => d.domain !== domain);
     localStorage.setItem('domains', JSON.stringify(domains));
   }

   getDomainsFromStorage(): WhoisStorageModel {
     const domains = localStorage.getItem('domains');
     if (domains) {
       return JSON.parse(domains);
     }
     return { domains: [] };
   }

   getDomainFromStorage(domain: string): WhoisResponseModel {
     const domains = this.getDomainsFromStorage();
     if(domains.domains.length > 0) {
       return domains.domains.find(d => d.domain === domain) || {} as WhoisResponseModel;
     }
     return {} as WhoisResponseModel;
   }

   updateDomainInStorage(domain: WhoisResponseModel) {
     const domains = this.getDomainsFromStorage();
     domains.domains = domains.domains.map(d => {
       if(d.domain === domain.domain) {
         returndomain;
       }
       return d;
     });
     localStorage.setItem('domains', JSON.stringify(domains));
   }
}

At the route level, we will essentially have two sections:

  1. The home page, with the search form.
  2. The domain history listing page.

Since a request may require the user to wait, we define a loader component to display while waiting.

// src/app/shared/components/loader/loader.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoaderComponent } from './loader.component';



@NgModule({
   declarations: [
     LoaderComponent
   ],
   imports: [
     CommonModule
   ],
   exports: [
     LoaderComponent
   ]
})
export class LoaderModule { }
// src/app/shared/components/loader/loader.component.ts

import { Component, Input } from '@angular/core';

@Component({
   selector: 'app-loader',
   templateUrl: './loader.component.html',
   styleUrls: ['./loader.component.css']
})
export class LoaderComponent {
     @Input() visible = false;
}
<!-- src/app/shared/components/loader/loader.component.html-->

<div class="app-loader" [class.visible]="visible">
     <div class="loader"></div>
</div>
/* src/app/shared/components/loader/loader.component.css */

.app-loader {
   position: fixed;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: rgba(0, 0, 0, 0.6);
   display: none;
   align-items: center;
   justify-content: center;
}

.app-loader.visible {
   display: flex;
}

.loader {
   border: 5px solid #fff;
   border-top: 5px solid #f36668;
   border-radius: 50%;
   width: 50px;
   height: 50px;
   animation: spin 2s linear infinite;
}

@keyframes spin {
   0% {
     transform: rotate(0deg);
   }
   100% {
     transform: rotate(360deg);
   }
}

Now we can define the home page component:

// src/app/features/home/home.component.ts

import {Component} from '@angular/core';
import {WhoisService} from '../../core/services/whois.service';
import {StorageService} from "../../core/services/storage.service";
import {WhoisResponseModel} from '../../core/models/whois-response.model';
import {WhoisResponseDto} from '../../core/dto/whois-response.dto';

@Component({
   selector: 'app-home',
   templateUrl: './home.component.html',
   styleUrls: ['./home.component.css'],
   providers: [WhoisService, StorageService]
})
export class HomeComponent {

   domain = '';
   whoisResponse: WhoisResponseModel = {} as WhoisResponseModel;
   errorMessage = '';
   showLoader = false;

   constructor(private whoisService: WhoisService, private storageService: StorageService) {
   }

   handleWhoisSearch() {
     this.errorMessage = '';
     this.whoisResponse = {} as WhoisResponseModel;
     this.showLoader = true;

     if (this.storageService.isDomainInStorage(this.domain)) {
       this.whoisResponse = this.storageService.getDomainFromStorage(this.domain);
       this.showLoader = false;
       return;
     }
     this.whoisService.getWhoisData(this.domain).subscribe({
       next: (response: WhoisResponseDto) => {
         this.whoisResponse = this.whoisService.parseWhoisData(response.result);
         this.showLoader = false;
         this.storageService.addDomainToStorage(this.domain, this.whoisResponse);
         window.location.href = '/domains';
       },
       error: (error) => {
         this.errorMessage = error.error.error;
         this.showLoader = false;
       }
     });
   }
}

If the domain is already present in the history, we return the result immediately. The relevant HTML template could be the following:


<!-- src/app/features/home/home.component.html --><

<form (ngSubmit)="handleWhoisSearch()">
     <div class="form-group">
         <input [(ngModel)]="domain" type="text" id="domain" name="domain" placeholder="Enter a domain name">
         <button type="submit" class="btn btn-primary">Whois</button>
     </div>
     <div *ngIf="errorMessage" class="alert alert-danger">{{errorMessage}}</div>
     <div class="app-whois-result" *ngIf="whoisResponse.domain">
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Domain</div>
             <div class="app-whois-result__value">{{whoisResponse.domain}}</div>
         </div>
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Status</div>
             <div class="app-whois-result__value">{{whoisResponse.status}}</div>
         </div>
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Registrar</div>
             <div class="app-whois-result__value">{{whoisResponse.registrar.name}}</div>
         </div>
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Created</div>
             <div class="app-whois-result__value">{{whoisResponse.created}}</div>
         </div>
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Updated</div>
             <div class="app-whois-result__value">{{whoisResponse.updated}}</div>
         </div>
         <div class="app-whois-result__item">
             <div class="app-whois-result__label">Expires</div>
             <div class="app-whois-result__value">{{whoisResponse.expires}}</div>
         </div>
     </div>
</form>
<app-loader [visible]="showLoader"></app-loader>

The domains page will allow the user to view saved domains, remove them from history or update them by making a new WHOIS request. The reference component is the following:


// src/app/features/domains/domains.component.ts

import { Component } from '@angular/core';
import { StorageService } from "../../core/services/storage.service";
import { WhoisStorageModel } from "../../core/models/whois-storage.model";
import { WhoisService } from "../../core/services/whois.service";
import { WhoisResponseModel } from "../../core/models/whois-response.model";
import { WhoisResponseDto } from "../../core/dto/whois-response.dto";

@Component({
   selector: 'app-domains',
   templateUrl: './domains.component.html',
   styleUrls: ['./domains.component.css'],
   providers: [StorageService, WhoisService]
})
export class DomainsComponent {
     data: WhoisStorageModel = {} as WhoisStorageModel;
     loaderVisible = false;

     constructor(private storageService: StorageService, private whoisService: WhoisService) {
     }

     removeDomain(domain: string) {
       this.storageService.removeDomainFromStorage(domain);
       this.data = this.storageService.getDomainsFromStorage();
     }

     updateDomain(domain: WhoisResponseModel) {
       const domainName = domain.domain;
       this.loaderVisible = true;
       this.whoisService.getWhoisData(domainName).subscribe({
         next: (response: WhoisResponseDto) => {
           const whoisResponse = this.whoisService.parseWhoisData(response.result);
           this.storageService.updateDomainInStorage(whoisResponse);
           this.data = this.storageService.getDomainsFromStorage();
           this.loaderVisible = false;
         },
         error: (error) => {
           console.log(error);
           this.loaderVisible = false;
         }
       });
     }

     ngOnInit() {
       this.data = this.storageService.getDomainsFromStorage();
     }
}

// src/app/features/domains/domains.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomainsComponent } from './domains.component';
import { LoaderModule } from "../../shared/components/loader/loader.module";


@NgModule({
   declarations: [
     DomainsComponent
   ],
   imports: [
     CommonModule,
     LoaderModule
   ],
   exports: [
     DomainsComponent
   ]
})
export class DomainsModule { }

The reference HTML template could be the following:

<!-- src/app/features/domains/domains.component.html -->

<section class="domains">
     <header>
         <h1>Domains</h1>
     </header>
     <p *ngIf="data.domains.length === 0">No domains found</p>
      <ul *ngIf="data.domains.length > 0">
           <li *ngFor="let domain of data.domains.reverse()">
             <div class="domain">
               <div class="name">{{ domain.domain }}</div>
               <div><strong>Status:</strong> <span>{{ domain.status }}</span></div>
               <div>
                 <strong>Expires:</strong> <span>{{ domain.expires }}</span>
               </div>
               <ofv>
                 <strong>Created:</strong> <span>{{ domain.created }}</span>
               </div>
               <div>
                 <strong>Updated:</strong> <span>{{ domain.updated }}</span>
               </div>
               <div>
                 <strong>Registrar:</strong> <span>{{ domain.registrar.name }}</span>
               </div>
               <div class="domain-actions">
                 <button class="remove-domain" (click)="removeDomain(domain.domain)">Remove</button>
                 <button class="update-domain" (click)="updateDomain(domain)">Update</button>
               </div>

             </div>
           </li>
       </ul>
</section>
<app-loader [visible]="loaderVisible"></app-loader>

Finally, we enable our routes in the dedicated file of our application:

// src/app/core/components/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from '../../../features/home/home.component';
import { DomainsComponent } from '../../../features/domains/domains.component';

const routes: Routes = [{
   path: '',
   component: HomeComponent,
   title: 'Whois NIC IT'
}, {
   path: 'domains',
   component: DomainsComponent,
   title: 'Domains - Whois NIC IT'
}];

@NgModule({
   imports: [RouterModule.forRoot(routes)],
   exports: [RouterModule]
})
export class AppRoutingModule { }

Obviously let's not forget to modify the main module of our app:


// src/app/core/components/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from "@angular/common/http";

import { AppRoutingModule } from './app-routing.module';
import { HomeModule } from "../../../features/home/home.module";
import { DomainsModule } from "../../../features/domains/domains.module";
import { AppComponent } from './app.component';

@NgModule({
   declarations: [
     AppComponent
   ],
   imports: [
     BrowserModule,
     AppRoutingModule,
     HttpClientModule,
     HomeModule,
     DomainsModule
   ],
   providers: [],
   bootstrap: [AppComponent]
})
export class AppModule { }

Conclusions

While the task may appear daunting at first glance, in reality once you understand the anatomy of a WHOIS request and the output produced, developing an application that uses it is a relatively straightforward task with Angular and Go.

Repository

whois-nic-it-app