main.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. package main
  2. import (
  3. "archive/zip"
  4. "bytes"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/tls"
  8. "crypto/x509"
  9. "crypto/x509/pkix"
  10. "encoding/base64"
  11. "encoding/pem"
  12. "errors"
  13. "fmt"
  14. "math/big"
  15. "net"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "reflect"
  20. "strings"
  21. "time"
  22. "github.com/gin-gonic/gin"
  23. )
  24. type tlsGenerateRequest struct {
  25. CommonName string `json:"commonName"`
  26. Organization string `json:"organization"`
  27. OrganizationalUnit string `json:"organizationalUnit"`
  28. Locality string `json:"locality"`
  29. State string `json:"state"`
  30. Country string `json:"country"`
  31. DNSNames string `json:"dnsNames"`
  32. ValidDays int `json:"validDays"`
  33. KeySize int `json:"keySize"`
  34. }
  35. type dnsLookupRequest struct {
  36. Host string `json:"host"`
  37. }
  38. type sslCheckRequest struct {
  39. URL string `json:"url"`
  40. }
  41. type pemCheckRequest struct {
  42. PEM string `json:"pem"`
  43. }
  44. type apiError struct {
  45. Error string `json:"error"`
  46. }
  47. func main() {
  48. port := getenv("PORT", "8080")
  49. certFile := strings.TrimSpace(os.Getenv("TLS_CERT_FILE"))
  50. keyFile := strings.TrimSpace(os.Getenv("TLS_KEY_FILE"))
  51. router := newRouter()
  52. addr := ":" + port
  53. var err error
  54. if certFile != "" || keyFile != "" {
  55. if certFile == "" || keyFile == "" {
  56. panic("both TLS_CERT_FILE and TLS_KEY_FILE must be set to enable TLS")
  57. }
  58. err = router.RunTLS(addr, certFile, keyFile)
  59. } else {
  60. err = router.Run(addr)
  61. }
  62. if err != nil {
  63. panic(err)
  64. }
  65. }
  66. func newRouter() *gin.Engine {
  67. router := gin.Default()
  68. router.LoadHTMLGlob("templates/*")
  69. router.Static("/static", "./static")
  70. _ = router.SetTrustedProxies(nil)
  71. router.GET("/", func(c *gin.Context) {
  72. c.HTML(http.StatusOK, "index.html", gin.H{
  73. "title": "Network Tools",
  74. })
  75. })
  76. api := router.Group("/api")
  77. {
  78. api.POST("/tls/generate", handleTLSGenerate)
  79. api.POST("/dns/lookup", handleDNSLookup)
  80. api.POST("/ssl/check", handleSSLCheck)
  81. api.POST("/pem/check", handlePEMCheck)
  82. }
  83. return router
  84. }
  85. func handleTLSGenerate(c *gin.Context) {
  86. var req tlsGenerateRequest
  87. if err := c.ShouldBindJSON(&req); err != nil {
  88. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  89. return
  90. }
  91. commonName := strings.TrimSpace(req.CommonName)
  92. if commonName == "" {
  93. c.JSON(http.StatusBadRequest, apiError{Error: "commonName is required"})
  94. return
  95. }
  96. validDays := req.ValidDays
  97. if validDays <= 0 {
  98. validDays = 365
  99. }
  100. keySize := normalizeKeySize(req.KeySize)
  101. if keySize == 0 {
  102. c.JSON(http.StatusBadRequest, apiError{Error: "keySize must be one of 2048, 3072, or 4096"})
  103. return
  104. }
  105. dnsNames := splitCSV(req.DNSNames)
  106. if len(dnsNames) == 0 {
  107. dnsNames = []string{commonName}
  108. }
  109. subject := pkix.Name{
  110. CommonName: commonName,
  111. Organization: splitCSV(req.Organization),
  112. OrganizationalUnit: splitCSV(req.OrganizationalUnit),
  113. Locality: splitCSV(req.Locality),
  114. Province: splitCSV(req.State),
  115. Country: splitCSV(req.Country),
  116. }
  117. certPEM, keyPEM, publicKeyPEM, csrPEM, parsed, err := generateCertificate(subject, dnsNames, validDays, keySize)
  118. if err != nil {
  119. c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
  120. return
  121. }
  122. baseName := sanitizeFilename(commonName)
  123. zipBytes, err := buildTLSArchive(baseName, certPEM, keyPEM, publicKeyPEM, csrPEM)
  124. if err != nil {
  125. c.JSON(http.StatusInternalServerError, apiError{Error: err.Error()})
  126. return
  127. }
  128. c.JSON(http.StatusOK, gin.H{
  129. "certificatePem": string(certPEM),
  130. "selfSignedCertificatePem": string(certPEM),
  131. "privateKeyPem": string(keyPEM),
  132. "publicKeyPem": string(publicKeyPEM),
  133. "csrPem": string(csrPEM),
  134. "subject": parsed.Subject.String(),
  135. "issuer": parsed.Issuer.String(),
  136. "serialNumber": parsed.SerialNumber.String(),
  137. "notBefore": parsed.NotBefore.Format(time.RFC3339),
  138. "notAfter": parsed.NotAfter.Format(time.RFC3339),
  139. "dnsNames": parsed.DNSNames,
  140. "keySize": keySize,
  141. "isSelfSigned": parsed.Subject.String() == parsed.Issuer.String(),
  142. "organization": parsed.Subject.Organization,
  143. "ou": parsed.Subject.OrganizationalUnit,
  144. "locality": parsed.Subject.Locality,
  145. "state": parsed.Subject.Province,
  146. "country": parsed.Subject.Country,
  147. "zipFilename": baseName + ".zip",
  148. "zipBase64": base64.StdEncoding.EncodeToString(zipBytes),
  149. })
  150. }
  151. func handleDNSLookup(c *gin.Context) {
  152. var req dnsLookupRequest
  153. if err := c.ShouldBindJSON(&req); err != nil {
  154. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  155. return
  156. }
  157. host := normalizeHost(req.Host)
  158. if host == "" {
  159. c.JSON(http.StatusBadRequest, apiError{Error: "host is required"})
  160. return
  161. }
  162. ips, ipErr := net.LookupIP(host)
  163. txt, txtErr := net.LookupTXT(host)
  164. mx, mxErr := net.LookupMX(host)
  165. ns, nsErr := net.LookupNS(host)
  166. cname, cnameErr := net.LookupCNAME(host)
  167. if ipErr != nil && txtErr != nil && mxErr != nil && nsErr != nil && cnameErr != nil {
  168. c.JSON(http.StatusBadGateway, apiError{Error: fmt.Sprintf("lookup failed for %s", host)})
  169. return
  170. }
  171. ipStrings := make([]string, 0, len(ips))
  172. for _, ip := range ips {
  173. ipStrings = append(ipStrings, ip.String())
  174. }
  175. txtStrings := make([]string, 0, len(txt))
  176. txtStrings = append(txtStrings, txt...)
  177. mxStrings := make([]gin.H, 0, len(mx))
  178. for _, record := range mx {
  179. mxStrings = append(mxStrings, gin.H{
  180. "host": record.Host,
  181. "pref": record.Pref,
  182. })
  183. }
  184. nsStrings := make([]string, 0, len(ns))
  185. for _, record := range ns {
  186. nsStrings = append(nsStrings, record.Host)
  187. }
  188. errorsByType := gin.H{}
  189. if ipErr != nil {
  190. errorsByType["a_aaaa"] = ipErr.Error()
  191. }
  192. if txtErr != nil {
  193. errorsByType["txt"] = txtErr.Error()
  194. }
  195. if mxErr != nil {
  196. errorsByType["mx"] = mxErr.Error()
  197. }
  198. if nsErr != nil {
  199. errorsByType["ns"] = nsErr.Error()
  200. }
  201. if cnameErr != nil {
  202. errorsByType["cname"] = cnameErr.Error()
  203. }
  204. c.JSON(http.StatusOK, gin.H{
  205. "host": host,
  206. "ips": ipStrings,
  207. "txt": txtStrings,
  208. "mx": mxStrings,
  209. "ns": nsStrings,
  210. "cname": strings.TrimSuffix(cname, "."),
  211. "errors": errorsByType,
  212. })
  213. }
  214. func handleSSLCheck(c *gin.Context) {
  215. var req sslCheckRequest
  216. if err := c.ShouldBindJSON(&req); err != nil {
  217. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  218. return
  219. }
  220. targetURL := strings.TrimSpace(req.URL)
  221. if targetURL == "" {
  222. c.JSON(http.StatusBadRequest, apiError{Error: "url is required"})
  223. return
  224. }
  225. parsedURL, err := normalizeURL(targetURL)
  226. if err != nil {
  227. c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
  228. return
  229. }
  230. host := parsedURL.Hostname()
  231. port := parsedURL.Port()
  232. if port == "" {
  233. port = "443"
  234. }
  235. conn, err := tls.DialWithDialer(
  236. &net.Dialer{Timeout: 10 * time.Second},
  237. "tcp",
  238. net.JoinHostPort(host, port),
  239. &tls.Config{ServerName: host},
  240. )
  241. if err != nil {
  242. c.JSON(http.StatusBadGateway, apiError{Error: err.Error()})
  243. return
  244. }
  245. defer conn.Close()
  246. state := conn.ConnectionState()
  247. if len(state.PeerCertificates) == 0 {
  248. c.JSON(http.StatusBadGateway, apiError{Error: "no peer certificates returned"})
  249. return
  250. }
  251. leaf := state.PeerCertificates[0]
  252. certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leaf.Raw})
  253. chain := make([]gin.H, 0, len(state.PeerCertificates))
  254. for _, cert := range state.PeerCertificates {
  255. chain = append(chain, gin.H{
  256. "subject": cert.Subject.String(),
  257. "issuer": cert.Issuer.String(),
  258. "serialNumber": cert.SerialNumber.String(),
  259. "notBefore": cert.NotBefore.Format(time.RFC3339),
  260. "notAfter": cert.NotAfter.Format(time.RFC3339),
  261. "dnsNames": cert.DNSNames,
  262. })
  263. }
  264. c.JSON(http.StatusOK, gin.H{
  265. "url": parsedURL.String(),
  266. "serverName": host,
  267. "version": tlsVersionName(state.Version),
  268. "cipherSuite": tls.CipherSuiteName(state.CipherSuite),
  269. "negotiatedProtocol": state.NegotiatedProtocol,
  270. "certificatePem": string(certPEM),
  271. "chain": chain,
  272. })
  273. }
  274. func handlePEMCheck(c *gin.Context) {
  275. var req pemCheckRequest
  276. if err := c.ShouldBindJSON(&req); err != nil {
  277. c.JSON(http.StatusBadRequest, apiError{Error: "invalid request body"})
  278. return
  279. }
  280. input := strings.TrimSpace(req.PEM)
  281. if input == "" {
  282. c.JSON(http.StatusBadRequest, apiError{Error: "pem is required"})
  283. return
  284. }
  285. blocks, err := inspectPEM(input)
  286. if err != nil {
  287. c.JSON(http.StatusBadRequest, apiError{Error: err.Error()})
  288. return
  289. }
  290. c.JSON(http.StatusOK, gin.H{
  291. "blocks": blocks,
  292. "count": len(blocks),
  293. })
  294. }
  295. func generateCertificate(subject pkix.Name, dnsNames []string, validDays int, keySize int) ([]byte, []byte, []byte, []byte, *x509.Certificate, error) {
  296. privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
  297. if err != nil {
  298. return nil, nil, nil, nil, nil, err
  299. }
  300. serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
  301. serialNumber, err := rand.Int(rand.Reader, serialLimit)
  302. if err != nil {
  303. return nil, nil, nil, nil, nil, err
  304. }
  305. notBefore := time.Now().UTC()
  306. template := &x509.Certificate{
  307. SerialNumber: serialNumber,
  308. Subject: subject,
  309. NotBefore: notBefore,
  310. NotAfter: notBefore.Add(time.Duration(validDays) * 24 * time.Hour),
  311. KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
  312. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
  313. BasicConstraintsValid: true,
  314. DNSNames: dnsNames,
  315. }
  316. derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
  317. if err != nil {
  318. return nil, nil, nil, nil, nil, err
  319. }
  320. csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
  321. Subject: subject,
  322. SignatureAlgorithm: x509.SHA256WithRSA,
  323. DNSNames: dnsNames,
  324. }, privateKey)
  325. if err != nil {
  326. return nil, nil, nil, nil, nil, err
  327. }
  328. publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
  329. if err != nil {
  330. return nil, nil, nil, nil, nil, err
  331. }
  332. certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
  333. keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
  334. publicKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes})
  335. csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
  336. parsedCert, err := x509.ParseCertificate(derBytes)
  337. if err != nil {
  338. return nil, nil, nil, nil, nil, err
  339. }
  340. return certPEM, keyPEM, publicKeyPEM, csrPEM, parsedCert, nil
  341. }
  342. func buildTLSArchive(baseName string, certPEM []byte, keyPEM []byte, publicKeyPEM []byte, csrPEM []byte) ([]byte, error) {
  343. var buffer bytes.Buffer
  344. archive := zip.NewWriter(&buffer)
  345. files := []struct {
  346. name string
  347. content []byte
  348. }{
  349. {name: baseName + ".crt.pem", content: certPEM},
  350. {name: baseName + ".key.pem", content: keyPEM},
  351. {name: baseName + ".pub.pem", content: publicKeyPEM},
  352. {name: baseName + ".csr.pem", content: csrPEM},
  353. }
  354. for _, file := range files {
  355. writer, err := archive.Create(file.name)
  356. if err != nil {
  357. return nil, err
  358. }
  359. if _, err := writer.Write(file.content); err != nil {
  360. return nil, err
  361. }
  362. }
  363. if err := archive.Close(); err != nil {
  364. return nil, err
  365. }
  366. return buffer.Bytes(), nil
  367. }
  368. func inspectPEM(input string) ([]gin.H, error) {
  369. var blocks []gin.H
  370. remaining := []byte(input)
  371. for len(strings.TrimSpace(string(remaining))) > 0 {
  372. block, rest := pem.Decode(remaining)
  373. if block == nil {
  374. return nil, errors.New("unable to decode PEM data")
  375. }
  376. inspected := gin.H{
  377. "pemType": block.Type,
  378. }
  379. switch block.Type {
  380. case "CERTIFICATE":
  381. cert, err := x509.ParseCertificate(block.Bytes)
  382. if err != nil {
  383. return nil, err
  384. }
  385. inspected["kind"] = "certificate"
  386. inspected["subject"] = cert.Subject.String()
  387. inspected["issuer"] = cert.Issuer.String()
  388. inspected["serialNumber"] = cert.SerialNumber.String()
  389. inspected["notBefore"] = cert.NotBefore.Format(time.RFC3339)
  390. inspected["notAfter"] = cert.NotAfter.Format(time.RFC3339)
  391. inspected["dnsNames"] = cert.DNSNames
  392. inspected["isCA"] = cert.IsCA
  393. inspected["signatureAlgorithm"] = cert.SignatureAlgorithm.String()
  394. inspected["publicKeyAlgorithm"] = cert.PublicKeyAlgorithm.String()
  395. case "CERTIFICATE REQUEST", "NEW CERTIFICATE REQUEST":
  396. csr, err := x509.ParseCertificateRequest(block.Bytes)
  397. if err != nil {
  398. return nil, err
  399. }
  400. inspected["kind"] = "certificate_request"
  401. inspected["subject"] = csr.Subject.String()
  402. inspected["dnsNames"] = csr.DNSNames
  403. inspected["emailAddresses"] = csr.EmailAddresses
  404. inspected["signatureAlgorithm"] = csr.SignatureAlgorithm.String()
  405. inspected["publicKeyAlgorithm"] = csr.PublicKeyAlgorithm.String()
  406. default:
  407. keyInfo, err := inspectKeyBlock(block)
  408. if err != nil {
  409. return nil, err
  410. }
  411. for key, value := range keyInfo {
  412. inspected[key] = value
  413. }
  414. }
  415. blocks = append(blocks, inspected)
  416. remaining = rest
  417. }
  418. if len(blocks) == 0 {
  419. return nil, errors.New("no PEM blocks found")
  420. }
  421. return blocks, nil
  422. }
  423. func inspectKeyBlock(block *pem.Block) (gin.H, error) {
  424. switch block.Type {
  425. case "RSA PRIVATE KEY":
  426. key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
  427. if err != nil {
  428. return nil, err
  429. }
  430. return gin.H{
  431. "kind": "private_key",
  432. "algorithm": "RSA",
  433. "size": key.N.BitLen(),
  434. }, nil
  435. case "EC PRIVATE KEY":
  436. key, err := x509.ParseECPrivateKey(block.Bytes)
  437. if err != nil {
  438. return nil, err
  439. }
  440. return gin.H{
  441. "kind": "private_key",
  442. "algorithm": "EC",
  443. "curve": key.Curve.Params().Name,
  444. }, nil
  445. case "PRIVATE KEY":
  446. key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
  447. if err != nil {
  448. return nil, err
  449. }
  450. return describePrivateKey(key), nil
  451. case "PUBLIC KEY":
  452. key, err := x509.ParsePKIXPublicKey(block.Bytes)
  453. if err != nil {
  454. return nil, err
  455. }
  456. return describePublicKey(key), nil
  457. default:
  458. return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
  459. }
  460. }
  461. func describePrivateKey(key any) gin.H {
  462. switch typed := key.(type) {
  463. case *rsa.PrivateKey:
  464. return gin.H{
  465. "kind": "private_key",
  466. "algorithm": "RSA",
  467. "size": typed.N.BitLen(),
  468. }
  469. default:
  470. return gin.H{
  471. "kind": "private_key",
  472. "algorithm": reflect.TypeOf(key).String(),
  473. }
  474. }
  475. }
  476. func describePublicKey(key any) gin.H {
  477. switch typed := key.(type) {
  478. case *rsa.PublicKey:
  479. return gin.H{
  480. "kind": "public_key",
  481. "algorithm": "RSA",
  482. "size": typed.N.BitLen(),
  483. }
  484. default:
  485. return gin.H{
  486. "kind": "public_key",
  487. "algorithm": reflect.TypeOf(key).String(),
  488. }
  489. }
  490. }
  491. func splitCSV(input string) []string {
  492. parts := strings.Split(input, ",")
  493. values := make([]string, 0, len(parts))
  494. for _, part := range parts {
  495. trimmed := strings.TrimSpace(part)
  496. if trimmed != "" {
  497. values = append(values, trimmed)
  498. }
  499. }
  500. return values
  501. }
  502. func normalizeHost(input string) string {
  503. value := strings.TrimSpace(input)
  504. value = strings.TrimPrefix(value, "https://")
  505. value = strings.TrimPrefix(value, "http://")
  506. value = strings.Trim(value, "/")
  507. if host, _, err := net.SplitHostPort(value); err == nil {
  508. return host
  509. }
  510. if parsed, err := url.Parse("https://" + value); err == nil {
  511. return parsed.Hostname()
  512. }
  513. return value
  514. }
  515. func normalizeURL(input string) (*url.URL, error) {
  516. value := strings.TrimSpace(input)
  517. if value == "" {
  518. return nil, errors.New("url is required")
  519. }
  520. if !strings.Contains(value, "://") {
  521. value = "https://" + value
  522. }
  523. parsed, err := url.Parse(value)
  524. if err != nil {
  525. return nil, err
  526. }
  527. if parsed.Hostname() == "" {
  528. return nil, errors.New("url must include a hostname")
  529. }
  530. return parsed, nil
  531. }
  532. func tlsVersionName(version uint16) string {
  533. switch version {
  534. case tls.VersionTLS10:
  535. return "TLS 1.0"
  536. case tls.VersionTLS11:
  537. return "TLS 1.1"
  538. case tls.VersionTLS12:
  539. return "TLS 1.2"
  540. case tls.VersionTLS13:
  541. return "TLS 1.3"
  542. default:
  543. return fmt.Sprintf("0x%x", version)
  544. }
  545. }
  546. func normalizeKeySize(value int) int {
  547. switch value {
  548. case 0, 2048:
  549. return 2048
  550. case 3072:
  551. return 3072
  552. case 4096:
  553. return 4096
  554. default:
  555. return 0
  556. }
  557. }
  558. func sanitizeFilename(value string) string {
  559. trimmed := strings.TrimSpace(strings.ToLower(value))
  560. if trimmed == "" {
  561. return "certificate"
  562. }
  563. var builder strings.Builder
  564. lastDash := false
  565. for _, r := range trimmed {
  566. isAlphaNum := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
  567. if isAlphaNum {
  568. builder.WriteRune(r)
  569. lastDash = false
  570. continue
  571. }
  572. if !lastDash {
  573. builder.WriteRune('-')
  574. lastDash = true
  575. }
  576. }
  577. result := strings.Trim(builder.String(), "-")
  578. if result == "" {
  579. return "certificate"
  580. }
  581. return result
  582. }
  583. func getenv(key, fallback string) string {
  584. value := strings.TrimSpace(os.Getenv(key))
  585. if value == "" {
  586. return fallback
  587. }
  588. return value
  589. }