Atually uploading this somewhere :)

This commit is contained in:
Dan Jones 2022-05-10 19:39:48 +01:00
commit 8bd5d14fb0
5 changed files with 323 additions and 0 deletions

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# slink
A URL shortener
## design
I made this abomination an "API" with the intention of people being able to integrate it into their own scripts and tools.
The `slink-keys.fish` script populates a token in every users home directory that they can use to create shortened URLs.
`shortlink` is a example script showing how to use the "API"
a request json looks like:
```json
{"url":"https://example.com/some/really/long/path"}
```

52
shortlink Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/fish
read pass < ~/.slink
set baseurl "https://heathens.club/u"
#set baseurl "http://localhost:8080"
function curl
command curl -s -H "Authentication:$pass" $argv
end
function list
for item in (curl "$baseurl/admin/list" | jq -r 'to_entries | .[] | "\(.key)|\(.value.url)"')
set item (string replace "|" " " $item)
echo -e $item
end
end
function delete -a short
printf "deleting %s\n" $short
set payload (jq -ncr --arg short $short '.url = $short' )
curl "$baseurl/admin/del" -d $payload
end
function prune
for link in (curl "$baseurl/admin/list" | jq -r 'to_entries | .[] | "\(.key)|\(.value.url)"')
set short (string split "|" $link)[1]
set url (string split "|" $link)[2]
curl -sI "$url" ^| grep "404"
and delete $short
or echo "leaving $short alone"
end
end
switch $argv[1]
case new
set payload (jq -ncr --arg url "$argv[2]" '.url = $url')
set link (curl "$baseurl/admin/create" -d $payload)
printf "%s\n" "$link"
case prune
prune
case list
list
case delete
delete $argv[2]
case "*"
printf "%s\n" "--- shortlink ---"
printf "%s\n" "new <link>"
printf "%s\n" "list"
printf "%s\n" "delete <shortcode>"
printf "%s\n" "prune"
end

33
slink-keys.fish Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/fish
# Automatically create and distribute slink API keys to local users
# Regenerate their api key if the file containing them was deleted.
set users_file users.txt
set key_len 40
set key_file '.slink'
for user in (find /home -mindepth 1 -maxdepth 1 -type d)
set user (basename $user)
if test \! -f /home/$user/$key_file
printf "user %s has no key file... " "$user"
printf "%s\n" "checking users.txt"
if test (grep $user $users_file)
printf "%s has existing api key, will clear\n" "$user"
sed -i "/^$user|.*/d" $users_file
printf "%s|%s\n" $user (pwgen -ns $key_len 1) >> $users_file
else
printf "%s doesn't have a key already, will generate\n" "$user"
printf "%s|%s\n" $user (pwgen -ns $key_len 1) >> $users_file
end
end
end
for line in (cat $users_file)
set line (string split "|" $line)
set user $line[1]
set key $line[2]
touch "/home/$user/$key_file"
chown $user "/home/$user/$key_file"
chmod 600 "/home/$user/$key_file"
printf "%s\n" "$key" > "/home/$user/$key_file"
end

209
slink.go Normal file
View File

@ -0,0 +1,209 @@
package main
import (
"log"
"net/http"
"bufio"
"io/ioutil"
"encoding/json"
"time"
"os"
"math/rand"
"unsafe"
"strings"
"flag"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIdxBits = 6
letterIdxMask = 1<<letterIdxBits - 1
letterIdxMax = 63 / letterIdxBits
)
var src = rand.NewSource(time.Now().UnixNano())
type Url struct{
Long string `json:"url"`
Timestamp int64 `json:"timestamp"`
User string `json:"user"`
}
type Link struct{
Long string `json:"url"`
Timestamp int64 `json:"timestamp"`
}
var StorageMap = make(map[string]Url)
func RandString(n int) string {
b := make([]byte, n)
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return *(*string)(unsafe.Pointer(&b))
}
func logSetup(port string,base string,storage string,usersFile string,host string) {
log.Printf("Server will run on: %s\n",port)
log.Printf("Server will use: %s\n", base)
log.Printf("Server will use: %s\n", storage)
log.Printf("Reading users from: %s\n",usersFile)
log.Printf("Using hostname: %s\n",host)
}
func healthHandler(w http.ResponseWriter, r *http.Request){
w.WriteHeader(200)
}
func getToken(token string, usersFile string) (user string) {
user = ""
file, err := os.Open(usersFile)
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
splitstring := strings.Split(scanner.Text(),"|")
if splitstring[1] == token {
user = splitstring[0]
}
}
return user
}
func main() {
Port := flag.String("port","8080","Port to listen on")
base := flag.String("base","/","Basepath /example/")
storage := flag.String("storage","storage.json","Storage json file")
usersFile := flag.String("users","users.txt","file for users and keys")
proto := flag.String("proto","https","https | http")
host := flag.String("host","localhost","hostname")
flag.Parse()
logSetup(*Port, *base, *storage, *usersFile, *host)
if _, err := os.Stat(*storage); err == nil {
log.Println("Loading data from storage file")
data,_ := ioutil.ReadFile (*storage)
json.Unmarshal(data,&StorageMap)
}
log.Printf("%+v",StorageMap)
ListenAddress := ":"+*Port
http.HandleFunc(*base, func (w http.ResponseWriter, r *http.Request){
short := strings.TrimPrefix(r.RequestURI,*base)
log.Printf(short)
var long string
if val,ok := StorageMap[short]; ok {
long = val.Long
}else{
w.WriteHeader(404)
return
}
http.Redirect(w,r,long,301)
})
http.HandleFunc(*base+"health",healthHandler)
http.HandleFunc(*base+"admin/create", func (w http.ResponseWriter, r *http.Request){
authHeader,hasAuthHeader := r.Header["Authentication"]
if hasAuthHeader {
user := getToken(authHeader[0],*usersFile)
if user != ""{
log.Printf("user authed: %s",user)
body,_ := ioutil.ReadAll(r.Body)
defer r.Body.Close()
var rq Url
err := json.Unmarshal(body,&rq)
if err != nil {
log.Printf(err.Error())
w.WriteHeader(500)
w.Write([]byte("Json parse error"))
return
}
if rq.Long == "" {
w.WriteHeader(500)
w.Write([]byte("No Url provided"))
return
}
rq.Timestamp = time.Now().Unix()
rq.User = user
short := RandString(8)
log.Println(short)
StorageMap[short] = rq
var rrr strings.Builder
rrr.WriteString(*proto)
rrr.WriteString("://")
rrr.WriteString(*host)
rrr.WriteString(*base)
rrr.WriteString(short)
rrr.WriteString("\n")
w.Write([]byte(rrr.String()))
jsondump,_ := json.Marshal(StorageMap)
ioutil.WriteFile(*storage,jsondump,0600)
}else{
w.WriteHeader(401)
}
}else{
w.WriteHeader(401)
}
})
http.HandleFunc(*base+"admin/list",func (w http.ResponseWriter, r *http.Request){
authHeader,hasAuthHeader := r.Header["Authentication"]
if hasAuthHeader {
user := getToken(authHeader[0],*usersFile)
log.Printf("user authed: %s",user)
if user != ""{
userMap := make(map[string]Link)
for key,link := range StorageMap {
if link.User == user {
userMap[key] = Link{
Long: link.Long,
Timestamp: link.Timestamp,
}
}
}
jsondump,_ := json.Marshal(userMap)
w.Write([]byte(jsondump))
}else{
w.WriteHeader(401)
}
}else{
w.WriteHeader(401)
}
})
http.HandleFunc(*base+"admin/del",func (w http.ResponseWriter, r *http.Request){
authHeader,hasAuthHeader := r.Header["Authentication"]
if hasAuthHeader {
user := getToken(authHeader[0],*usersFile)
if user != ""{
log.Printf("user authed: %s",user)
body,_ := ioutil.ReadAll(r.Body)
defer r.Body.Close()
var rq Url
json.Unmarshal(body,&rq)
log.Printf(rq.Long)
if StorageMap[rq.Long].User == user {
delete(StorageMap,rq.Long)
}else{
w.WriteHeader(404)
return
}
w.WriteHeader(200)
jsondump,_ := json.Marshal(StorageMap)
ioutil.WriteFile(*storage,jsondump,0600)
}else{
w.WriteHeader(401)
}
}else{
w.WriteHeader(401)
}
})
if err := http.ListenAndServe(ListenAddress, nil); err != nil {
panic(err)
}
}

12
slink.openrc Executable file
View File

@ -0,0 +1,12 @@
#!/sbin/openrc-run
description="Slink"
pidfile="/var/run/slink.pid"
command="/usr/bin/slink"
command_args="--base '/u/' --storage /var/lib/slink/storage.json --users /var/lib/slink/users.txt"
command_background="true"
command_user="slink"
depend() {
need net
}