Atually uploading this somewhere :)
This commit is contained in:
commit
8bd5d14fb0
|
@ -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"}
|
||||
```
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue