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")
}