git.sophuwu.com > urlshort   
              409
            
             package main

import (
	"context"
	"encoding/json"
	"errors"
	"github.com/valyala/fastjson"
	bolt "go.etcd.io/bbolt"
	"io"
	"math/rand"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"time"
)

var urlDB *bolt.DB
var wordList []string
var virustotalKey string
var key string

type urlStruc struct {
	Date time.Time
	Path string
	Url  string
	Uses int
}

func fec(err error) { // fatal error check
	if err != nil {
		println(err.Error())
		urlDB.Close()
		os.Exit(1)
	}
}

func openFiles() {
	b, err := os.ReadFile("wordlist")
	fec(err)
	if len(b) == 0 {
		println("wordlist is empty")
		os.Exit(1)
	}
	wordList = strings.Split(string(b), "\n")

	b, err = os.ReadFile("virustotal.key")
	fec(err)
	if len(b) == 0 {
		println("virustotal.key is empty")
		os.Exit(1)
	}
	virustotalKey = string(b)

	b, err = os.ReadFile("key")
	fec(err)
	if len(b) == 0 {
		println("key is empty")
		os.Exit(1)
	}
	key = string(b)

	var db *bolt.DB
	db, err = bolt.Open("urls.db", 0600, nil)
	fec(err)
	fec(db.Update(func(tx *bolt.Tx) error {
		_, err = tx.CreateBucketIfNotExists([]byte("urls"))
		return err
	}))
	urlDB = db
}

func getWord() string {
	return wordList[rand.Intn(len(wordList))]
}

func readURL(words string) (urlStruc, error) {
	if words == "" {
		return urlStruc{}, errors.New("No words provided")
	}
	var url urlStruc
	var err error
	err = urlDB.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("urls"))
		return json.Unmarshal(b.Get([]byte(words)), &url)
	})
	return url, err
}

func checkURL(words string) bool {
	url, err := readURL(words)
	if err == nil && url.Url != "" {
		return true
	}
	return false
}

func writeURL(url urlStruc) error {
	jsonUrl, err := json.Marshal(url)
	if err != nil {
		return err
	}
	return urlDB.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("urls"))
		return b.Put([]byte(url.Path), jsonUrl)
	})
}

func apiRequest(method string, url string, body io.Reader, headers map[string]string) []byte {
	req, _ := http.NewRequest(method, url, body)
	for k, v := range headers {
		req.Header.Add(k, v)
	}
	resp, _ := http.DefaultClient.Do(req)
	jay_son, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	return jay_son
}

func testUrlSafe(inspectionUrl string) bool {
	var jsonDec fastjson.Parser
	url := "https://www.virustotal.com/api/v3/urls"

	headers := map[string]string{
		"content-type": "application/x-www-form-urlencoded",
		"accept":       "application/json",
		"x-apikey":     virustotalKey,
	}

	jsonB := apiRequest("POST", url, strings.NewReader("url="+inspectionUrl), headers)

	jay_son, _ := jsonDec.ParseBytes(jsonB)
	url = string(jay_son.GetStringBytes("data", "links", "self"))

	time.Sleep(30 * time.Second)

	delete(headers, "content-type")
	jsonB = apiRequest("GET", url, nil, headers)

	jay_son, _ = jsonDec.ParseBytes(jsonB)
	harmless := jay_son.GetFloat64("data", "attributes", "stats", "harmless")
	undetected := jay_son.GetFloat64("data", "attributes", "stats", "undetected")
	malicious := jay_son.GetFloat64("data", "attributes", "stats", "malicious")
	suspicious := jay_son.GetFloat64("data", "attributes", "stats", "suspicious")

	return (malicious+suspicious)/(harmless+undetected+malicious+suspicious) > 0.25
}

func http404(w http.ResponseWriter) {
	w.WriteHeader(404)
	b, err := os.ReadFile("html/404.html")
	if err != nil {
		b = []byte("404 not found")
		w.Header().Set("Content-Type", "text/plain")
	} else {
		w.Header().Set("Content-Type", "text/html")
	}
	w.Write(b)
}

func http500(w http.ResponseWriter) {
	w.WriteHeader(500)
	b, err := os.ReadFile("html/500.html")
	if err != nil {
		b = []byte("500 internal server error")
		w.Header().Set("Content-Type", "text/plain")
	} else {
		w.Header().Set("Content-Type", "text/html")
	}
	w.Write(b)
}

func hec(w http.ResponseWriter, err error, code int) bool {
	if err == nil {
		return false
	}
	switch code {
	case 404:
		http404(w)
		break
	case 500:
		http500(w)
		break
	}
	return true
}

func intStr(n int) string {
	if n == 0 {
		return "0"
	}
	var s, sign string
	if n < 0 {
		sign = "-"
		n = -n
	}
	for ; n > 0; n /= 10 {
		s = string(n%10+int('0')) + s
	}
	return sign + s
}

func infoPage(w http.ResponseWriter, url urlStruc) {
	w.WriteHeader(200)
	w.Header().Set("Content-Type", "text/html")
	b, err := os.ReadFile("html/info.html")
	if hec(w, err, 500) {
		return
	}
	s := strings.ReplaceAll(string(b), "{{url}}", url.Url)
	s = strings.ReplaceAll(s, "{{path}}", url.Path)
	s = strings.ReplaceAll(s, "{{uses}}", intStr(url.Uses))
	s = strings.ReplaceAll(s, "{{date}}", url.Date.Format("2006-01-02 15:04:05"))
	w.Write([]byte(s))
}

func createPage(w http.ResponseWriter, api bool, code int, resp string) {
	w.WriteHeader(code)
	if api {
		w.Header().Set("Content-Type", "text/plain")
		w.Write([]byte(resp))
		return
	}
	s := "Error loading HTML\nYour request was still processed.\nRequest response: " + resp + "\n"
	b, err := os.ReadFile("html/create.html")
	if err != nil {
		w.Header().Set("Content-Type", "text/plain")
		w.Write([]byte(s))
		return
	}
	s = strings.ReplaceAll(string(b), "{{response}}", resp)
	if code != 200 {
		s = strings.ReplaceAll(s, "{{title}}", "Your request failed:")
	} else {
		s = strings.ReplaceAll(s, "{{title}}", "Your short URL:")
	}
	w.Header().Set("Content-Type", "text/html")
	w.Write([]byte(s))
}

func deleteDangerUrl(url urlStruc) {
	if !testUrlSafe(url.Url) {
		return
	}
	url.Url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
	writeURL(url)
}

var locky sync.Mutex

func createHandler(w http.ResponseWriter, r *http.Request) {
	locky.Lock()
	defer locky.Unlock()
	var err error
	var url urlStruc
	var api bool = false
	url.Date = time.Now()
	url.Uses = 0
	err = r.ParseForm()
	if hec(w, err, 500) {
		return
	}
	if r.Form.Get("mode") == "api" {
		api = true
	}
	if r.Form.Get("key") != key {
		time.Sleep(1 * time.Second)
		createPage(w, api, 401, "Error: Invalid key")
		return
	}
	url.Url = r.Form.Get("url")
	if url.Url == "" {
		createPage(w, api, 400, "Error: No URL provided")
		return
	}
	if !strings.Contains(url.Url, ".") || !(strings.HasPrefix(url.Url, "http://") || strings.HasPrefix(url.Url, "https://")) {
		createPage(w, api, 400, "Error: Invalid URL")
		return
	}
	url.Path = r.Form.Get("custom")
	if url.Path == "" {
		url.Path = getWord() + "-" + getWord() + "-"
		for {
			url.Path += getWord()
			if !checkURL(url.Path) {
				break
			}
			url.Path += "-"
		}
	} else {
		if checkURL(url.Path) {
			createPage(w, api, 400, "Error: Custom URL already exists")
			return
		}
	}
	go deleteDangerUrl(url)
	err = writeURL(url)
	if hec(w, err, 500) {
		return
	}
	createPage(w, api, 200, r.Host+"/"+url.Path)
}

func httpHandler(w http.ResponseWriter, r *http.Request) {
	if len(r.URL.Path) < 2 {
		if r.Method == "POST" {
			createHandler(w, r)
			return
		}
		http.ServeFile(w, r, "html/index.html")
		return
	}
	path := strings.TrimSuffix(r.URL.Path, "/")
	path = strings.TrimPrefix(path, r.URL.Host)
	path = strings.TrimPrefix(path, "/")
	var err error
	if path == "favicon.ico" {
		_, err = os.Stat("favicon.ico")
		if hec(w, err, 404) {
			return
		}
		http.ServeFile(w, r, "favicon.ico")
		return
	}
	var info bool = false
	if strings.HasSuffix(path, "/info") {
		info = true
		path = strings.TrimSuffix(path, "/info")
	}
	var url urlStruc
	url, err = readURL(path)
	if err != nil || url.Url == "" {
		http404(w)
		return
	}
	if info {
		infoPage(w, url)
		return
	}
	http.Redirect(w, r, url.Url, http.StatusFound)
	url.Uses++
	_ = writeURL(url)
}

const helpstring = ` [options]
options:
    -i[str]
    	str is the address (or hostname thereof) the server will to listen to.
    	if -i option is given with no str, the server will listen on all
    	possible addresses.
       	if this option is not present, 127.0.0.1 will be used by default
    -p[num]
    	num is the port number to use for http traffic
`

func parseArgs(iface, port *string) {
	for _,arg := range os.Args[1:] {
		if len(arg)>1 && arg[0]=='-' {
			switch arg[1] {
			case 'i':
				*iface=arg[2:]
				continue
			case 'p':
				*port=arg[2:]
				continue
			}
		}
		println("usage: "+os.Args[0]+helpstring)
		os.Exit(1)
	}
}

func main() {
	var (
		iface  string = "127.0.0.1"
		port   string = "8088"
		err    error
		server http.Server
	)

	parseArgs(&iface,&port)

	println("Starting server on " + iface + ":" + port)

	openFiles()

	http.HandleFunc("/", httpHandler)

	server = http.Server{
		Addr:    iface + ":" + port,
		Handler: nil,
	}

	go func() {
		err = server.ListenAndServe()
		if err != http.ErrServerClosed && err != nil {
			urlDB.Close()
			panic(err)
		}
	}()
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt)
	<-quit

	urlDB.Close()
	server.Shutdown(context.Background())
	print("\nServer stopped\n\n")
}