Files
link-shortener/internal/handlers/links.go
Giovanni Rezcjikov 8befcc11c1
Some checks failed
continuous-integration/drone/push Build is failing
feat: increased security
2026-02-21 21:05:47 +03:00

134 lines
2.7 KiB
Go

package handlers
import (
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"example.com/m/internal/services"
"github.com/gin-gonic/gin"
)
type LinksHandler struct {
service *services.LinksService
host string
port string
}
type CreateLinkResponse struct {
Status string `json:"status"`
ShortLink string `json:"short_link"`
OriginalLink string `json:"original_link"`
ExpiresIn string `json:"expires_in"`
}
func NewLinksHandler(service *services.LinksService, host string, port string) *LinksHandler {
return &LinksHandler{service: service, host: host, port: port}
}
func (h *LinksHandler) CreateLink(c *gin.Context) {
link := c.Param("link")
link = strings.TrimPrefix(link, "/")
if c.Request.URL.RawQuery != "" {
link += "?" + c.Request.URL.RawQuery
}
link, err := NormalizeURL(link)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "failed", "message": "invalid link"})
return
}
id, err := h.service.CreateLink(link)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed", "message": "failed to create link"})
return
}
address := fmt.Sprintf("https://%s/r/%s", h.host, id)
response := CreateLinkResponse{
Status: "success",
ShortLink: address,
OriginalLink: link,
ExpiresIn: "30d",
}
c.JSON(http.StatusOK, response)
}
func (h *LinksHandler) Redirect(c *gin.Context) {
id := c.Param("id")
original, err := h.service.GetLink(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "failed", "message": "link not found"})
return
}
c.Redirect(http.StatusFound, original)
}
func NormalizeURL(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", errors.New("empty URL")
}
if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
raw = "https://" + raw
}
u, err := url.Parse(raw)
if err != nil {
return "", errors.New("invalid URL")
}
if u.Host == "" {
return "", errors.New("invalid host in URL")
}
host := u.Hostname()
if isPrivateHost(host) {
return "", errors.New("URLs pointing to private/internal addresses are not allowed")
}
return u.String(), nil
}
func isPrivateHost(host string) bool {
ip := net.ParseIP(host)
if ip == nil {
addrs, err := net.LookupHost(host)
if err != nil || len(addrs) == 0 {
return false
}
ip = net.ParseIP(addrs[0])
if ip == nil {
return false
}
}
privateRanges := []string{
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"0.0.0.0/8",
"::1/128",
"fc00::/7",
"fe80::/10",
}
for _, cidr := range privateRanges {
_, network, _ := net.ParseCIDR(cidr)
if network.Contains(ip) {
return true
}
}
return false
}