good morning!!!!

Skip to content
Snippets Groups Projects
linkpreview.go 4.66 KiB
Newer Older
yinebebt's avatar
yinebebt committed
package main

import (
	"encoding/json"
yinebebt's avatar
yinebebt committed
	"golang.org/x/net/html"
yinebebt's avatar
yinebebt committed
	"net/http"
yinebebt's avatar
yinebebt committed
	"strings"
	"time"
type linkPreview struct {
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	ImageURL    string `json:"image_url,omitempty"`
var client = &http.Client{
	Timeout: time.Second * 2,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
}

// previewLink handles the HTTP request, fetches the URL, and returns the link preview.
func previewLink(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet && r.Method != http.MethodHead {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// check authorization
	uid, challenge, err := authHttpRequest(r)
	if err != nil {
		http.Error(w, "invalid auth secret", http.StatusBadRequest)
yinebebt's avatar
yinebebt committed
		return
	}
		http.Error(w, "user not authenticated", http.StatusUnauthorized)
	u := r.URL.Query().Get("url")
	if u == "" {
		http.Error(w, "Missing 'url' query parameter", http.StatusBadRequest)
	pu, err := url.Parse(u)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := validateURL(pu); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	req, err := http.NewRequest(http.MethodGet, u, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
yinebebt's avatar
yinebebt committed
	resp, err := client.Do(req)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
yinebebt's avatar
yinebebt committed
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { // StatusCode != 20X
		http.Error(w, "Non-OK HTTP status", http.StatusBadGateway)
	body := http.MaxBytesReader(nil, resp.Body, 2*1024) // 2KB limit
	if cc := resp.Header.Get("Cache-Control"); cc != "" {
		w.Header().Set("Cache-Control", cc)
	}

	w.Header().Set("Content-Type", "application/json")
	if r.Method == http.MethodHead {
		w.WriteHeader(http.StatusOK)
		return
	}
	if err := json.NewEncoder(w).Encode(extractMetadata(body)); err != nil {
yinebebt's avatar
yinebebt committed
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
	}
}

func extractMetadata(body io.Reader) *linkPreview {

	tokenizer := html.NewTokenizer(body)
	for {
		switch tokenizer.Next() {
		case html.ErrorToken:

		case html.StartTagToken, html.SelfClosingTagToken:
			tag, hasAttr := tokenizer.TagName()
			tagName := atom.Lookup(tag)
			if tagName == atom.Meta && hasAttr {
				var name, property, content string
				for {
					key, val, moreAttr := tokenizer.TagAttr()
					switch atom.String(key) {
						name = string(val)
						property = string(val)
						content = string(val)
					}
					if !moreAttr {
						break
				if content != "" {
					if strings.HasPrefix(property, "og:") {
						switch property {
						case "og:title":
							preview.Title = content
						case "og:description":
							preview.Description = content
						case "og:image":
							preview.ImageURL = content
						}
					} else if name == "description" && preview.Description == "" {
			} else if tagName == atom.Title {
			if inTitleTag {
				if preview.Title == "" {
					preview.Title = tokenizer.Token().Data
				}

		case html.EndTagToken:
			inTitleTag = false
		if preview.Title != "" && preview.Description != "" && preview.ImageURL != "" {
func validateURL(u *url.URL) error {
	if u.Scheme != "http" && u.Scheme != "https" {
		return &url.Error{Op: "validate", Err: errors.New("invalid scheme")}
	if err != nil {
		return &url.Error{Op: "validate", Err: errors.New("invalid host")}
	}
	for _, ip := range ips {
		if ip.IsLoopback() || ip.IsPrivate() {
			return &url.Error{Op: "validate", Err: errors.New("non routable IP address")}

func sanitizePreview(preview linkPreview) *linkPreview {
	if utf8.RuneCountInString(preview.Title) > 80 {
		preview.Title = string([]rune(strings.TrimSpace(preview.Title))[:80])
	}
	if utf8.RuneCountInString(preview.Description) > 256 {
		preview.Description = string([]rune(strings.TrimSpace(preview.Description))[:256])
		preview.ImageURL = strings.TrimSpace(preview.ImageURL)[:2000]