Added experimental acme renew from Let's Encrypt

This commit is contained in:
Toby Chui
2023-06-23 23:09:10 +08:00
parent 594f75da97
commit 23eca5afae
24 changed files with 3470 additions and 30 deletions

288
src/mod/acme/acme.go Normal file
View File

@@ -0,0 +1,288 @@
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"imuslab.com/zoraxy/mod/utils"
)
// ACMEUser represents a user in the ACME system.
type ACMEUser struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
// GetEmail returns the email of the ACMEUser.
func (u *ACMEUser) GetEmail() string {
return u.Email
}
// GetRegistration returns the registration resource of the ACMEUser.
func (u ACMEUser) GetRegistration() *registration.Resource {
return u.Registration
}
// GetPrivateKey returns the private key of the ACMEUser.
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// ACMEHandler handles ACME-related operations.
type ACMEHandler struct {
DefaultAcmeServer string
Port string
}
// NewACME creates a new ACMEHandler instance.
func NewACME(acmeServer string, port string) *ACMEHandler {
return &ACMEHandler{
DefaultAcmeServer: acmeServer,
Port: port,
}
}
// ObtainCert obtains a certificate for the specified domains.
func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) {
log.Println("[ACME] Obtaining certificate...")
// generate private key
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Println(err)
return false, err
}
// create a admin user for our new generation
adminUser := ACMEUser{
Email: email,
key: privateKey,
}
// create config
config := lego.NewConfig(&adminUser)
// setup who is the issuer and the key type
config.CADirURL = a.DefaultAcmeServer
//Overwrite the CADir URL if set
if ca != "" {
caLinkOverwrite, err := loadCAApiServerFromName(ca)
if err == nil {
config.CADirURL = caLinkOverwrite
log.Println("[INFO] Using " + caLinkOverwrite + " for CA Directory URL")
} else {
return false, errors.New("CA " + ca + " is not supported. Please contribute to the source code and add this CA's directory link.")
}
}
config.Certificate.KeyType = certcrypto.RSA2048
client, err := lego.NewClient(config)
if err != nil {
log.Println(err)
return false, err
}
// setup how to receive challenge
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", a.Port))
if err != nil {
log.Println(err)
return false, err
}
// New users will need to register
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
log.Println(err)
return false, err
}
adminUser.Registration = reg
// obtain the certificate
request := certificate.ObtainRequest{
Domains: domains,
Bundle: true,
}
certificates, err := client.Certificate.Obtain(request)
if err != nil {
log.Println(err)
return false, err
}
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
err = ioutil.WriteFile("./certs/"+certificateName+".crt", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err
}
err = ioutil.WriteFile("./certs/"+certificateName+".key", certificates.PrivateKey, 0777)
if err != nil {
log.Println(err)
return false, err
}
return true, nil
}
// CheckCertificate returns a list of domains that are in expired certificates.
// It will return all domains that is in expired certificates
// *** if there is a vaild certificate contains the domain and there is a expired certificate contains the same domain
// it will said expired as well!
func (a *ACMEHandler) CheckCertificate() []string {
// read from dir
filenames, err := os.ReadDir("./certs/")
expiredCerts := []string{}
if err != nil {
log.Println(err)
return []string{}
}
for _, filename := range filenames {
certFilepath := filepath.Join("./certs/", filename.Name())
certBytes, err := os.ReadFile(certFilepath)
if err != nil {
// Unable to load this file
continue
} else {
// Cert loaded. Check its expiry time
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
for _, dnsName := range cert.DNSNames {
if !contains(expiredCerts, dnsName) {
expiredCerts = append(expiredCerts, dnsName)
}
}
if !contains(expiredCerts, cert.Subject.CommonName) {
expiredCerts = append(expiredCerts, cert.Subject.CommonName)
}
}
}
}
}
}
return expiredCerts
}
// return the current port number
func (a *ACMEHandler) Getport() string {
return a.Port
}
// contains checks if a string is present in a slice.
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// HandleGetExpiredDomains handles the HTTP GET request to retrieve the list of expired domains.
// It calls the CheckCertificate method to obtain the expired domains and sends a JSON response
// containing the list of expired domains.
func (a *ACMEHandler) HandleGetExpiredDomains(w http.ResponseWriter, r *http.Request) {
type ExpiredDomains struct {
Domain []string `json:"domain"`
}
info := ExpiredDomains{
Domain: a.CheckCertificate(),
}
js, _ := json.MarshalIndent(info, "", " ")
utils.SendJSONResponse(w, string(js))
}
// HandleRenewCertificate handles the HTTP GET request to renew a certificate for the provided domains.
// It retrieves the domains and filename parameters from the request, calls the ObtainCert method
// to renew the certificate, and sends a JSON response indicating the result of the renewal process.
func (a *ACMEHandler) HandleRenewCertificate(w http.ResponseWriter, r *http.Request) {
domainPara, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
filename, err := utils.PostPara(r, "filename")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
email, err := utils.PostPara(r, "email")
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
ca, err := utils.PostPara(r, "ca")
if err != nil {
log.Println("CA not set. Using default (Let's Encrypt)")
ca = "Let's Encrypt"
}
domains := strings.Split(domainPara, ",")
result, err := a.ObtainCert(domains, filename, email, ca)
if err != nil {
utils.SendErrorResponse(w, jsonEscape(err.Error()))
return
}
utils.SendJSONResponse(w, strconv.FormatBool(result))
}
// Escape JSON string
func jsonEscape(i string) string {
b, err := json.Marshal(i)
if err != nil {
log.Println("Unable to escape json data: " + err.Error())
return i
}
s := string(b)
return s[1 : len(s)-1]
}
// Helper function to check if a port is in use
func IsPortInUse(port int) bool {
address := fmt.Sprintf(":%d", port)
listener, err := net.Listen("tcp", address)
if err != nil {
return true // Port is in use
}
defer listener.Close()
return false // Port is not in use
}

24
src/mod/acme/acme_test.go Normal file
View File

@@ -0,0 +1,24 @@
package acme_test
import (
"fmt"
"testing"
"imuslab.com/zoraxy/mod/acme"
)
// Test if the issuer extraction is working
func TestExtractIssuerNameFromPEM(t *testing.T) {
pemFilePath := "test/stackoverflow.pem"
expectedIssuer := "Let's Encrypt"
issuerName, err := acme.ExtractIssuerNameFromPEM(pemFilePath)
fmt.Println(issuerName)
if err != nil {
t.Errorf("Error extracting issuer name: %v", err)
}
if issuerName != expectedIssuer {
t.Errorf("Unexpected issuer name. Expected: %s, Got: %s", expectedIssuer, issuerName)
}
}

348
src/mod/acme/autorenew.go Normal file
View File

@@ -0,0 +1,348 @@
package acme
import (
"encoding/json"
"errors"
"log"
"net/http"
"net/mail"
"os"
"path/filepath"
"strings"
"time"
"imuslab.com/zoraxy/mod/utils"
)
/*
autorenew.go
This script handle auto renew
*/
type AutoRenewConfig struct {
Enabled bool //Automatic renew is enabled
Email string //Email for acme
RenewAll bool //Renew all or selective renew with the slice below
FilesToRenew []string //If RenewAll is false, renew these certificate files
}
type AutoRenewer struct {
ConfigFilePath string
CertFolder string
AcmeHandler *ACMEHandler
RenewerConfig *AutoRenewConfig
RenewTickInterval int64
TickerstopChan chan bool
}
type ExpiredCerts struct {
Domains []string
Filepath string
CA string
}
// Create an auto renew agent, require config filepath and auto scan & renew interval (seconds)
// Set renew check interval to 0 for auto (1 day)
func NewAutoRenewer(config string, certFolder string, renewCheckInterval int64, AcmeHandler *ACMEHandler) (*AutoRenewer, error) {
if renewCheckInterval == 0 {
renewCheckInterval = 86400 //1 day
}
//Load the config file. If not found, create one
if !utils.FileExists(config) {
//Create one
os.MkdirAll(filepath.Dir(config), 0775)
newConfig := AutoRenewConfig{
RenewAll: true,
FilesToRenew: []string{},
}
js, _ := json.MarshalIndent(newConfig, "", " ")
err := os.WriteFile(config, js, 0775)
if err != nil {
return nil, errors.New("Failed to create acme auto renewer config: " + err.Error())
}
}
renewerConfig := AutoRenewConfig{}
content, err := os.ReadFile(config)
if err != nil {
return nil, errors.New("Failed to open acme auto renewer config: " + err.Error())
}
err = json.Unmarshal(content, &renewerConfig)
if err != nil {
return nil, errors.New("Malformed acme config file: " + err.Error())
}
//Create an Auto renew object
thisRenewer := AutoRenewer{
ConfigFilePath: config,
CertFolder: certFolder,
AcmeHandler: AcmeHandler,
RenewerConfig: &renewerConfig,
RenewTickInterval: renewCheckInterval,
}
if thisRenewer.RenewerConfig.Enabled {
//Start the renew ticker
thisRenewer.StartAutoRenewTicker()
//Check and renew certificate on startup
go thisRenewer.CheckAndRenewCertificates()
}
return &thisRenewer, nil
}
func (a *AutoRenewer) StartAutoRenewTicker() {
//Stop the previous ticker if still running
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
time.Sleep(1 * time.Second)
ticker := time.NewTicker(time.Duration(a.RenewTickInterval) * time.Second)
done := make(chan bool)
//Start the ticker to check and renew every x seconds
go func(a *AutoRenewer) {
for {
select {
case <-done:
return
case <-ticker.C:
log.Println("Check and renew certificates in progress")
a.CheckAndRenewCertificates()
}
}
}(a)
a.TickerstopChan = done
}
func (a *AutoRenewer) StopAutoRenewTicker() {
if a.TickerstopChan != nil {
a.TickerstopChan <- true
}
a.TickerstopChan = nil
}
// Handle update auto renew domains
// Set opr for different mode of operations
// opr = setSelected -> Enter a list of file names (or matching rules) for auto renew
// opr = setAuto -> Set to use auto detect certificates and renew
func (a *AutoRenewer) HandleSetAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
opr, err := utils.GetPara(r, "opr")
if err != nil {
utils.SendErrorResponse(w, "Operation not set")
return
}
if opr == "setSelected" {
files, err := utils.PostPara(r, "domains")
if err != nil {
utils.SendErrorResponse(w, "Domains is not defined")
return
}
//Parse it int array of string
matchingRuleFiles := []string{}
err = json.Unmarshal([]byte(files), &matchingRuleFiles)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Update the configs
a.RenewerConfig.RenewAll = false
a.RenewerConfig.FilesToRenew = matchingRuleFiles
a.saveRenewConfigToFile()
utils.SendOK(w)
} else if opr == "setAuto" {
a.RenewerConfig.RenewAll = true
a.saveRenewConfigToFile()
utils.SendOK(w)
}
}
// if auto renew all is true (aka auto scan), it will return []string{"*"}
func (a *AutoRenewer) HandleLoadAutoRenewDomains(w http.ResponseWriter, r *http.Request) {
results := []string{}
if a.RenewerConfig.RenewAll {
//Auto pick which cert to renew.
results = append(results, "*")
} else {
//Manually set the files to renew
results = a.RenewerConfig.FilesToRenew
}
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
}
func (a *AutoRenewer) HandleRenewNow(w http.ResponseWriter, r *http.Request) {
}
func (a *AutoRenewer) HandleAutoRenewEnable(w http.ResponseWriter, r *http.Request) {
val, err := utils.PostPara(r, "enable")
if err != nil {
js, _ := json.Marshal(a.RenewerConfig.Enabled)
utils.SendJSONResponse(w, string(js))
} else {
if val == "true" {
//Check if the email is not empty
if a.RenewerConfig.Email == "" {
utils.SendErrorResponse(w, "Email is not set")
return
}
a.RenewerConfig.Enabled = true
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew enabled")
a.StartAutoRenewTicker()
} else {
a.RenewerConfig.Enabled = false
a.saveRenewConfigToFile()
log.Println("[ACME] ACME auto renew disabled")
a.StopAutoRenewTicker()
}
}
}
func (a *AutoRenewer) HandleACMEEmail(w http.ResponseWriter, r *http.Request) {
email, err := utils.PostPara(r, "set")
if err != nil {
//Return the current email to user
js, _ := json.Marshal(a.RenewerConfig.Email)
utils.SendJSONResponse(w, string(js))
} else {
//Check if the email is valid
_, err := mail.ParseAddress(email)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//Set the new config
a.RenewerConfig.Email = email
a.saveRenewConfigToFile()
}
}
// Check and renew certificates. This check all the certificates in the
// certificate folder and return a list of certs that is renewed in this call
// Return string array with length 0 when no cert is expired
func (a *AutoRenewer) CheckAndRenewCertificates() ([]string, error) {
certFolder := a.CertFolder
files, err := os.ReadDir(certFolder)
if err != nil {
log.Println("Unable to renew certificates: " + err.Error())
return []string{}, err
}
expiredCertList := []*ExpiredCerts{}
if a.RenewerConfig.RenewAll {
//Scan and renew all
for _, file := range files {
if filepath.Ext(file.Name()) == ".crt" || filepath.Ext(file.Name()) == ".pem" {
//This is a public key file
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
//This cert is expired
CAName, err := ExtractIssuerName(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Unable to extract issuer name for cert " + file.Name())
continue
}
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
CA: CAName,
Domains: DNSName,
})
}
}
}
} else {
//Only renew those in the list
for _, file := range files {
fileName := file.Name()
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
if contains(a.RenewerConfig.FilesToRenew, certName) {
//This is the one to auto renew
certBytes, err := os.ReadFile(filepath.Join(certFolder, file.Name()))
if err != nil {
continue
}
if CertExpireSoon(certBytes) || CertIsExpired(certBytes) {
//This cert is expired
CAName, err := ExtractIssuerName(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Unable to extract issuer name for cert " + file.Name())
continue
}
DNSName, err := ExtractDomains(certBytes)
if err != nil {
//Maybe self signed. Ignore this
log.Println("Encounted error when trying to resolve DNS name for cert " + file.Name())
continue
}
expiredCertList = append(expiredCertList, &ExpiredCerts{
Filepath: filepath.Join(certFolder, file.Name()),
CA: CAName,
Domains: DNSName,
})
}
}
}
}
return a.renewExpiredDomains(expiredCertList)
}
// Renew the certificate by filename extract all DNS name from the
// certificate and renew them one by one by calling to the acmeHandler
func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, error) {
renewedCertFiles := []string{}
for _, expiredCert := range certs {
log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)")
fileName := filepath.Base(expiredCert.Filepath)
certName := fileName[:len(fileName)-len(filepath.Ext(fileName))]
_, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA)
if err != nil {
log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error())
} else {
log.Println("Successfully renewed " + filepath.Base(expiredCert.Filepath))
renewedCertFiles = append(renewedCertFiles, filepath.Base(expiredCert.Filepath))
}
}
return renewedCertFiles, nil
}
// Write the current renewer config to file
func (a *AutoRenewer) saveRenewConfigToFile() error {
js, _ := json.MarshalIndent(a.RenewerConfig, "", " ")
return os.WriteFile(a.ConfigFilePath, js, 0775)
}

45
src/mod/acme/ca.go Normal file
View File

@@ -0,0 +1,45 @@
package acme
/*
CA.go
This script load CA defination from embedded ca.json
*/
import (
_ "embed"
"encoding/json"
"errors"
"log"
)
// CA Defination, load from embeded json when startup
type CaDef struct {
Production map[string]string
Test map[string]string
}
//go:embed ca.json
var caJson []byte
var caDef CaDef = CaDef{}
func init() {
runtimeCaDef := CaDef{}
err := json.Unmarshal(caJson, &runtimeCaDef)
if err != nil {
log.Println("[ERR] Unable to unmarshal CA def from embedded file. You sure your ca.json is valid?")
return
}
caDef = runtimeCaDef
}
// Get the CA ACME server endpoint and error if not found
func loadCAApiServerFromName(caName string) (string, error) {
val, ok := caDef.Production[caName]
if !ok {
return "", errors.New("This CA is not supported")
}
return val, nil
}

15
src/mod/acme/ca.json Normal file
View File

@@ -0,0 +1,15 @@
{
"production": {
"Let's Encrypt": "https://acme-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.buypass.com/acme/directory",
"ZeroSSL": "https://acme.zerossl.com/v2/DV90",
"Google": "https://dv.acme-v02.api.pki.goog/directory"
},
"test":{
"Let's Encrypt": "https://acme-staging-v02.api.letsencrypt.org/directory",
"Buypass": "https://api.test4.buypass.no/acme/directory",
"Google": "https://dv.acme-v02.test-api.pki.goog/directory"
}
}

94
src/mod/acme/utils.go Normal file
View File

@@ -0,0 +1,94 @@
package acme
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"time"
)
// Get the issuer name from pem file
func ExtractIssuerNameFromPEM(pemFilePath string) (string, error) {
// Read the PEM file
pemData, err := ioutil.ReadFile(pemFilePath)
if err != nil {
return "", err
}
return ExtractIssuerName(pemData)
}
// Get the DNSName in the cert
func ExtractDomains(certBytes []byte) ([]string, error) {
domains := []string{}
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return []string{}, err
}
for _, dnsName := range cert.DNSNames {
if !contains(domains, dnsName) {
domains = append(domains, dnsName)
}
}
return domains, nil
}
return []string{}, errors.New("decode cert bytes failed")
}
func ExtractIssuerName(certBytes []byte) (string, error) {
// Parse the PEM block
block, _ := pem.Decode(certBytes)
if block == nil || block.Type != "CERTIFICATE" {
return "", fmt.Errorf("failed to decode PEM block containing certificate")
}
// Parse the certificate
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse certificate: %v", err)
}
// Extract the issuer name
issuer := cert.Issuer.Organization[0]
return issuer, nil
}
// Check if a cert is expired by public key
func CertIsExpired(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
elapsed := time.Since(cert.NotAfter)
if elapsed > 0 {
// if it is expired then add it in
// make sure it's uniqueless
return true
}
}
}
return false
}
func CertExpireSoon(certBytes []byte) bool {
block, _ := pem.Decode(certBytes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
expirationDate := cert.NotAfter
threshold := 14 * 24 * time.Hour // 14 days
timeRemaining := time.Until(expirationDate)
if timeRemaining <= threshold {
return true
}
}
}
return false
}

View File

@@ -3,9 +3,11 @@ package netutils
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/likexian/whois"
"imuslab.com/zoraxy/mod/utils"
)
@@ -46,6 +48,50 @@ func TraceRoute(targetIpOrDomain string, maxHops int) ([]string, error) {
return traceroute(targetIpOrDomain, maxHops)
}
func HandleWhois(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
utils.SendErrorResponse(w, "invalid target (domain or ip) address given")
return
}
raw, _ := utils.GetPara(r, "raw")
result, err := whois.Whois(targetIpOrDomain)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
if raw == "true" {
utils.SendTextResponse(w, result)
} else {
if isDomainName(targetIpOrDomain) {
//Is Domain
parsedOutput, err := ParseWHOISResponse(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
} else {
//Is IP
parsedOutput, err := ParseWhoisIpData(result)
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
js, _ := json.Marshal(parsedOutput)
utils.SendJSONResponse(w, string(js))
}
}
}
func HandlePing(w http.ResponseWriter, r *http.Request) {
targetIpOrDomain, err := utils.GetPara(r, "target")
if err != nil {
@@ -53,13 +99,44 @@ func HandlePing(w http.ResponseWriter, r *http.Request) {
return
}
results := []string{}
type MixedPingResults struct {
ICMP []string
TCP []string
UDP []string
}
results := MixedPingResults{
ICMP: []string{},
TCP: []string{},
UDP: []string{},
}
//Ping ICMP
for i := 0; i < 4; i++ {
realIP, pingTime, ttl, err := PingIP(targetIpOrDomain)
if err != nil {
results = append(results, "Reply from "+realIP+": "+err.Error())
results.ICMP = append(results.ICMP, "Reply from "+realIP+": "+err.Error())
} else {
results = append(results, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
results.ICMP = append(results.ICMP, fmt.Sprintf("Reply from %s: Time=%dms TTL=%d", realIP, pingTime.Milliseconds(), ttl))
}
}
//Ping TCP
for i := 0; i < 4; i++ {
pingTime, err := TCPPing(targetIpOrDomain)
if err != nil {
results.TCP = append(results.TCP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.TCP = append(results.TCP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
//Ping UDP
for i := 0; i < 4; i++ {
pingTime, err := UDPPing(targetIpOrDomain)
if err != nil {
results.UDP = append(results.UDP, "Reply from "+resolveIpFromDomain(targetIpOrDomain)+": "+err.Error())
} else {
results.UDP = append(results.UDP, fmt.Sprintf("Reply from %s: Time=%dms", resolveIpFromDomain(targetIpOrDomain), pingTime.Milliseconds()))
}
}
@@ -67,3 +144,16 @@ func HandlePing(w http.ResponseWriter, r *http.Request) {
utils.SendJSONResponse(w, string(js))
}
func resolveIpFromDomain(targetIpOrDomain string) string {
//Resolve target ip address
targetIpAddrString := ""
ipAddr, err := net.ResolveIPAddr("ip", targetIpOrDomain)
if err != nil {
targetIpAddrString = targetIpOrDomain
} else {
targetIpAddrString = ipAddr.IP.String()
}
return targetIpAddrString
}

View File

@@ -6,6 +6,39 @@ import (
"time"
)
// TCP ping
func TCPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("tcp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish TCP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// UDP Ping
func UDPPing(ipOrDomain string) (time.Duration, error) {
start := time.Now()
conn, err := net.DialTimeout("udp", ipOrDomain+":80", 3*time.Second)
if err != nil {
return 0, fmt.Errorf("failed to establish UDP connection: %v", err)
}
defer conn.Close()
elapsed := time.Since(start)
pingTime := elapsed.Round(time.Millisecond)
return pingTime, nil
}
// Traditional ICMP ping
func PingIP(ipOrDomain string) (string, time.Duration, int, error) {
ipAddr, err := net.ResolveIPAddr("ip", ipOrDomain)
if err != nil {

199
src/mod/netutils/whois.go Normal file
View File

@@ -0,0 +1,199 @@
package netutils
import (
"net"
"strings"
"time"
)
type WHOISResult struct {
DomainName string `json:"domainName"`
RegistryDomainID string `json:"registryDomainID"`
Registrar string `json:"registrar"`
UpdatedDate time.Time `json:"updatedDate"`
CreationDate time.Time `json:"creationDate"`
ExpiryDate time.Time `json:"expiryDate"`
RegistrantID string `json:"registrantID"`
RegistrantName string `json:"registrantName"`
RegistrantEmail string `json:"registrantEmail"`
AdminID string `json:"adminID"`
AdminName string `json:"adminName"`
AdminEmail string `json:"adminEmail"`
TechID string `json:"techID"`
TechName string `json:"techName"`
TechEmail string `json:"techEmail"`
NameServers []string `json:"nameServers"`
DNSSEC string `json:"dnssec"`
}
func ParseWHOISResponse(response string) (WHOISResult, error) {
result := WHOISResult{}
lines := strings.Split(response, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Domain Name:") {
result.DomainName = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:"))
} else if strings.HasPrefix(line, "Registry Domain ID:") {
result.RegistryDomainID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Domain ID:"))
} else if strings.HasPrefix(line, "Registrar:") {
result.Registrar = strings.TrimSpace(strings.TrimPrefix(line, "Registrar:"))
} else if strings.HasPrefix(line, "Updated Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Updated Date:"))
updatedDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.UpdatedDate = updatedDate
}
} else if strings.HasPrefix(line, "Creation Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Creation Date:"))
creationDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.CreationDate = creationDate
}
} else if strings.HasPrefix(line, "Registry Expiry Date:") {
dateStr := strings.TrimSpace(strings.TrimPrefix(line, "Registry Expiry Date:"))
expiryDate, err := time.Parse("2006-01-02T15:04:05Z", dateStr)
if err == nil {
result.ExpiryDate = expiryDate
}
} else if strings.HasPrefix(line, "Registry Registrant ID:") {
result.RegistrantID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Registrant ID:"))
} else if strings.HasPrefix(line, "Registrant Name:") {
result.RegistrantName = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Name:"))
} else if strings.HasPrefix(line, "Registrant Email:") {
result.RegistrantEmail = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Email:"))
} else if strings.HasPrefix(line, "Registry Admin ID:") {
result.AdminID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Admin ID:"))
} else if strings.HasPrefix(line, "Admin Name:") {
result.AdminName = strings.TrimSpace(strings.TrimPrefix(line, "Admin Name:"))
} else if strings.HasPrefix(line, "Admin Email:") {
result.AdminEmail = strings.TrimSpace(strings.TrimPrefix(line, "Admin Email:"))
} else if strings.HasPrefix(line, "Registry Tech ID:") {
result.TechID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Tech ID:"))
} else if strings.HasPrefix(line, "Tech Name:") {
result.TechName = strings.TrimSpace(strings.TrimPrefix(line, "Tech Name:"))
} else if strings.HasPrefix(line, "Tech Email:") {
result.TechEmail = strings.TrimSpace(strings.TrimPrefix(line, "Tech Email:"))
} else if strings.HasPrefix(line, "Name Server:") {
ns := strings.TrimSpace(strings.TrimPrefix(line, "Name Server:"))
result.NameServers = append(result.NameServers, ns)
} else if strings.HasPrefix(line, "DNSSEC:") {
result.DNSSEC = strings.TrimSpace(strings.TrimPrefix(line, "DNSSEC:"))
}
}
return result, nil
}
type WhoisIpLookupEntry struct {
NetRange string
CIDR string
NetName string
NetHandle string
Parent string
NetType string
OriginAS string
Organization Organization
RegDate time.Time
Updated time.Time
Ref string
}
type Organization struct {
OrgName string
OrgId string
Address string
City string
StateProv string
PostalCode string
Country string
/*
RegDate time.Time
Updated time.Time
OrgTechHandle string
OrgTechName string
OrgTechPhone string
OrgTechEmail string
OrgAbuseHandle string
OrgAbuseName string
OrgAbusePhone string
OrgAbuseEmail string
OrgRoutingHandle string
OrgRoutingName string
OrgRoutingPhone string
OrgRoutingEmail string
*/
}
func ParseWhoisIpData(data string) (WhoisIpLookupEntry, error) {
var entry WhoisIpLookupEntry = WhoisIpLookupEntry{}
var org Organization = Organization{}
lines := strings.Split(data, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "NetRange:") {
entry.NetRange = strings.TrimSpace(strings.TrimPrefix(line, "NetRange:"))
} else if strings.HasPrefix(line, "CIDR:") {
entry.CIDR = strings.TrimSpace(strings.TrimPrefix(line, "CIDR:"))
} else if strings.HasPrefix(line, "NetName:") {
entry.NetName = strings.TrimSpace(strings.TrimPrefix(line, "NetName:"))
} else if strings.HasPrefix(line, "NetHandle:") {
entry.NetHandle = strings.TrimSpace(strings.TrimPrefix(line, "NetHandle:"))
} else if strings.HasPrefix(line, "Parent:") {
entry.Parent = strings.TrimSpace(strings.TrimPrefix(line, "Parent:"))
} else if strings.HasPrefix(line, "NetType:") {
entry.NetType = strings.TrimSpace(strings.TrimPrefix(line, "NetType:"))
} else if strings.HasPrefix(line, "OriginAS:") {
entry.OriginAS = strings.TrimSpace(strings.TrimPrefix(line, "OriginAS:"))
} else if strings.HasPrefix(line, "Organization:") {
org.OrgName = strings.TrimSpace(strings.TrimPrefix(line, "Organization:"))
} else if strings.HasPrefix(line, "OrgId:") {
org.OrgId = strings.TrimSpace(strings.TrimPrefix(line, "OrgId:"))
} else if strings.HasPrefix(line, "Address:") {
org.Address = strings.TrimSpace(strings.TrimPrefix(line, "Address:"))
} else if strings.HasPrefix(line, "City:") {
org.City = strings.TrimSpace(strings.TrimPrefix(line, "City:"))
} else if strings.HasPrefix(line, "StateProv:") {
org.StateProv = strings.TrimSpace(strings.TrimPrefix(line, "StateProv:"))
} else if strings.HasPrefix(line, "PostalCode:") {
org.PostalCode = strings.TrimSpace(strings.TrimPrefix(line, "PostalCode:"))
} else if strings.HasPrefix(line, "Country:") {
org.Country = strings.TrimSpace(strings.TrimPrefix(line, "Country:"))
} else if strings.HasPrefix(line, "RegDate:") {
entry.RegDate, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "RegDate:")))
} else if strings.HasPrefix(line, "Updated:") {
entry.Updated, _ = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Updated:")))
} else if strings.HasPrefix(line, "Ref:") {
entry.Ref = strings.TrimSpace(strings.TrimPrefix(line, "Ref:"))
}
}
entry.Organization = org
return entry, nil
}
func parseDate(dateStr string) (time.Time, error) {
dateLayout := "2006-01-02"
date, err := time.Parse(dateLayout, strings.TrimSpace(dateStr))
if err != nil {
return time.Time{}, err
}
return date, nil
}
func isDomainName(input string) bool {
ip := net.ParseIP(input)
if ip != nil {
// Check if it's IPv4 or IPv6
if ip.To4() != nil {
return false
} else if ip.To16() != nil {
return false
}
}
_, err := net.LookupHost(input)
return err == nil
}

View File

@@ -12,15 +12,14 @@ import (
)
/*
Pathblock.go
Pathrules.go
This script block off some of the specific pathname in access
For example, this module can help you block request for a particular
apache directory or functional endpoints like /.well-known/ when you
are not using it
This script handle advance path settings and rules on particular
paths of the incoming requests
*/
type Options struct {
Enabled bool //If the pathrule is enabled.
ConfigFolder string //The folder to store the path blocking config files
}
@@ -41,7 +40,7 @@ type Handler struct {
}
// Create a new path blocker handler
func NewPathBlocker(options *Options) *Handler {
func NewPathRuleHandler(options *Options) *Handler {
//Create folder if not exists
if !utils.FileExists(options.ConfigFolder) {
os.Mkdir(options.ConfigFolder, 0775)