1
0
forked from tcp.direct/tcp.ac

adding index functionality, implementing gzip

This commit is contained in:
kayos@tcp.direct 2021-01-29 05:27:38 -08:00
parent 883f5193e0
commit 5595ec9404
12 changed files with 127 additions and 639 deletions

@ -1,40 +1,14 @@
package main package main
import ( import (
"github.com/prologic/bitcask" "fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/rs/zerolog"
"strconv" "strconv"
"fmt"
"os"
) )
////////////// global declarations
// datastores
var imgDB *bitcask.Bitcask
var hashDB *bitcask.Bitcask
var keyDB *bitcask.Bitcask
var urlDB *bitcask.Bitcask
var txtDB *bitcask.Bitcask
// config directives
var debugBool bool
var baseUrl string
var webPort string
var webIP string
var dbDir string
var logDir string
var uidSize int
var keySize int
// utilitarian globals
var s string
var fn string
var i int
var err error
var f *os.File
///////////////////////////////// /////////////////////////////////
func configRead() { func configRead() {
viper.SetConfigName("config") // filename without ext viper.SetConfigName("config") // filename without ext
viper.SetConfigType("toml") // also defines extension viper.SetConfigType("toml") // also defines extension
@ -51,8 +25,10 @@ func configRead() {
debugBool = viper.GetBool("global.debug") // we need to load the debug boolean first debugBool = viper.GetBool("global.debug") // we need to load the debug boolean first
// so we can output config directives // so we can output config directives
if debugBool { if debugBool {
log.Debug().Msg("Debug mode enabled")
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
log.Debug().Msg("Debug mode enabled")
} else {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
} }
s = "http.baseurl" s = "http.baseurl"

@ -1,7 +1,7 @@
title = "tcp.ac config" title = "tcp.ac config"
[global] [global]
debug = true debug = false
[http] [http]
baseurl = "http://127.0.0.1:8080/" baseurl = "http://127.0.0.1:8080/"

3
db.go

@ -1,11 +1,10 @@
package main package main
import ( import (
"github.com/prologic/bitcask"
"fmt" "fmt"
"github.com/prologic/bitcask"
) )
func dbInit() { func dbInit() {
opts := []bitcask.Option{ opts := []bitcask.Option{
bitcask.WithMaxValueSize(24 / 1024 / 1024), bitcask.WithMaxValueSize(24 / 1024 / 1024),

51
img.go

@ -1,19 +1,19 @@
package main package main
import ( import (
"bytes"
valid "github.com/asaskevich/govalidator" valid "github.com/asaskevich/govalidator"
exifremove "github.com/scottleedavis/go-exif-remove"
"golang.org/x/crypto/blake2b"
"github.com/twharmon/gouid"
"github.com/rs/zerolog/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
exifremove "github.com/scottleedavis/go-exif-remove"
"github.com/twharmon/gouid"
"golang.org/x/crypto/blake2b"
"image"
_ "image/gif" _ "image/gif"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"image"
"bytes"
"io"
) )
var fExt string var fExt string
@ -26,7 +26,9 @@ type Post struct {
func postUpload(c *gin.Context, id string, key string) { func postUpload(c *gin.Context, id string, key string) {
imgurl := baseUrl + "i/" + string(id) imgurl := baseUrl + "i/" + string(id)
keyurl := "duplicate" keyurl := "duplicate"
if key != "nil" { keyurl = baseUrl + "d/i/" + string(key) } if key != "nil" {
keyurl = baseUrl + "d/i/" + string(key)
}
log.Info().Str("func", "imgPost").Str("id", id).Str("status", "201").Str("imgurl", imgurl).Str("keyurl", keyurl) log.Info().Str("func", "imgPost").Str("id", id).Str("status", "201").Str("imgurl", imgurl).Str("keyurl", keyurl)
c.JSON(201, gin.H{"delkey": keyurl, "imgurl": imgurl}) c.JSON(201, gin.H{"delkey": keyurl, "imgurl": imgurl})
@ -38,14 +40,14 @@ func imgDel(c *gin.Context) {
log.Debug().Str("func", fn).Msg("Request received!") // received request log.Debug().Str("func", fn).Msg("Request received!") // received request
rKey := c.Param("key") rKey := c.Param("key")
if (len(rKey) != 16 || !valid.IsAlphanumeric(rKey)) { if len(rKey) != 16 || !valid.IsAlphanumeric(rKey) {
log.Error().Str("func", fn).Msg("delete request failed sanity check!") log.Error().Str("func", fn).Msg("delete request failed sanity check!")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
targetImg, _ := keyDB.Get([]byte(rKey)) targetImg, _ := keyDB.Get([]byte(rKey))
if (targetImg == nil || !strings.Contains(string(targetImg), "i.")) { if targetImg == nil || !strings.Contains(string(targetImg), "i.") {
log.Error().Str("func", fn).Str("rkey", rKey).Msg("no img delete entry found with provided key") log.Error().Str("func", fn).Str("rkey", rKey).Msg("no img delete entry found with provided key")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
@ -82,7 +84,6 @@ func imgDel(c *gin.Context) {
// we will delete the hash entry then and re-add then // we will delete the hash entry then and re-add then
} }
func imgView(c *gin.Context) { func imgView(c *gin.Context) {
fn = "imgView" fn = "imgView"
// the user can access their image with or without a file extension in URI // the user can access their image with or without a file extension in URI
@ -92,15 +93,16 @@ func imgView(c *gin.Context) {
if len(sUid) > 1 { if len(sUid) > 1 {
fExt = strings.ToLower(sUid[1]) fExt = strings.ToLower(sUid[1])
log.Debug().Str("func", fn).Str("ext", fExt).Msg("detected file extension") log.Debug().Str("func", fn).Str("ext", fExt).Msg("detected file extension")
if (fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif") { if fExt != "png" && fExt != "jpg" && fExt != "jpeg" && fExt != "gif" {
log.Error().Str("func", fn).Msg("Bad file extension!") log.Error().Str("func", fn).Msg("Bad file extension!")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
} }
} else { fExt = "nil" } } else {
fExt = "nil"
}
if !valid.IsAlphanumeric(rUid) || len(rUid) < 3 || len(rUid) > 16 {
if (!valid.IsAlphanumeric(rUid) || len(rUid) < 3 || len(rUid) > 16) {
log.Error().Str("func", fn).Msg("request discarded as invalid") // these limits should be variables eventually log.Error().Str("func", fn).Msg("request discarded as invalid") // these limits should be variables eventually
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
@ -115,17 +117,17 @@ func imgView(c *gin.Context) {
return return
} }
file := bytes.NewReader(fBytes) file := bytes.NewReader(fBytes)
imageFormat, ok := checkImage(file) imageFormat, ok := checkImage(file)
if !ok { if !ok {
errThrow(c, http.StatusBadRequest, "400", "400") errThrow(c, http.StatusBadRequest, "400", "400")
log.Error().Str("func", fn).Str("rUid", rUid).Msg("the file we grabbed is not an image!?") // not sure how a non image would get uploaded log.Error().Str("func", fn).Str("rUid", rUid).Msg("the file we grabbed is not an image!?") // not sure how a non image would get uploaded
return // however, better safe than sorry return // however, better safe than sorry
} else { log.Debug().Str("func",fn).Str("rUid",rUid).Str("imageFormat",imageFormat).Msg("Image format detected") } } else {
log.Debug().Str("func", fn).Str("rUid", rUid).Str("imageFormat", imageFormat).Msg("Image format detected")
}
if (fExt != "nil" && fExt != imageFormat) { // additional extension sanity check if fExt != "nil" && fExt != imageFormat { // additional extension sanity check
log.Error().Str("func", fn).Str("rUid", rUid).Msg("requested file extension does not match filetype") log.Error().Str("func", fn).Str("rUid", rUid).Msg("requested file extension does not match filetype")
errThrow(c, 400, "400", "400") errThrow(c, 400, "400", "400")
return return
@ -138,7 +140,6 @@ func imgView(c *gin.Context) {
log.Info().Str("func", fn).Str("rUid", rUid).Msg("Successful upload") log.Info().Str("func", fn).Str("rUid", rUid).Msg("Successful upload")
} }
func imgPost(c *gin.Context) { func imgPost(c *gin.Context) {
fn = "imgPost" fn = "imgPost"
@ -161,7 +162,9 @@ func imgPost(c *gin.Context) {
if !ok { if !ok {
errThrow(c, http.StatusBadRequest, "400", "input does not appear to be an image") errThrow(c, http.StatusBadRequest, "400", "input does not appear to be an image")
return return
} else { log.Debug().Str("func",fn).Msg("image file type detected") } } else {
log.Debug().Str("func", fn).Msg("image file type detected")
}
log.Debug().Str("func", fn).Msg("dumping byte form of file") log.Debug().Str("func", fn).Msg("dumping byte form of file")
fbytes, err := ioutil.ReadAll(file) fbytes, err := ioutil.ReadAll(file)
@ -205,18 +208,16 @@ func imgPost(c *gin.Context) {
uid := gouid.String(uidSize) // these should both be config directives eventually uid := gouid.String(uidSize) // these should both be config directives eventually
key := gouid.String(keySize) // generate delete key key := gouid.String(keySize) // generate delete key
// lets make sure that we don't clash even though its highly unlikely // lets make sure that we don't clash even though its highly unlikely
for uidRef, _ := imgDB.Get([]byte(uid)); uidRef != nil; { for uidRef, _ := imgDB.Get([]byte(uid)); uidRef != nil; {
log.Info().Str("func", fn).Msg(" uid already exists! generating new...") log.Info().Str("func", fn).Msg(" uid already exists! generating new...")
uid = gouid.String(5) uid = gouid.String(uidSize)
} }
for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; { for keyRef, _ := keyDB.Get([]byte(key)); keyRef != nil; {
log.Info().Str("func", fn).Msg(" delete key already exists! generating new...") log.Info().Str("func", fn).Msg(" delete key already exists! generating new...")
key = gouid.String(16) key = gouid.String(keySize)
} }
hashDB.Put([]byte(hash), []byte(uid)) // save checksum to db to prevent dupes in the future hashDB.Put([]byte(hash), []byte(uid)) // save checksum to db to prevent dupes in the future
log.Debug().Str("func", fn).Str("uid", uid).Msg("saving file to database") log.Debug().Str("func", fn).Str("uid", uid).Msg("saving file to database")

@ -44,9 +44,6 @@ func init() {
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
multi := zerolog.MultiLevelWriter(consoleWriter, lf) multi := zerolog.MultiLevelWriter(consoleWriter, lf)
log.Logger = zerolog.New(multi).With().Timestamp().Logger() log.Logger = zerolog.New(multi).With().Timestamp().Logger()
zerolog.SetGlobalLevel(zerolog.InfoLevel) // default is info and above
dbInit() dbInit()
} }

@ -1,6 +1,9 @@
package main package main
import ( import (
"github.com/gin-contrib/logger"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -15,7 +18,19 @@ func urlPost(c *gin.Context) {
func httpRouter() { func httpRouter() {
router := gin.New() router := gin.New()
router.MaxMultipartMemory = 16 << 20 router.MaxMultipartMemory = 16 << 20 // crude POST limit (fix this)
// use gzip compression unless someone requests something with an explicit extension
router.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPathsRegexs([]string{".*"})))
router.Use(logger.SetLogger()) // use our own logger
// static html and such
// workaround the issue where the router tries to handle /*
router.Static("/h", "./public")
router.StaticFile("/favicon.ico", "./public/favicon.ico")
router.GET("/", func(c *gin.Context) { c.Redirect(301, "h/") })
imgR := router.Group("/i") imgR := router.Group("/i")
{ {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,111 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title }}</title>
<style>
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #000;
color: #f8f8f8;
}
.hljs-comment,
.hljs-quote,
.hljs-meta {
color: #7c7c7c;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-tag,
.hljs-name {
color: #96cbfe;
}
.hljs-attribute,
.hljs-selector-id {
color: #ffffb6;
}
.hljs-string,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition {
color: #a8ff60;
}
.hljs-subst {
color: #daefa3;
}
.hljs-regexp,
.hljs-link {
color: #e9c062;
}
.hljs-title,
.hljs-section,
.hljs-type,
.hljs-doctag {
color: #ffffb6;
}
.hljs-symbol,
.hljs-bullet,
.hljs-variable,
.hljs-template-variable,
.hljs-literal {
color: #c6c5fe;
}
.hljs-number,
.hljs-deletion {
color:#ff73fd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
html,
body {
overflow: hidden;
}
html,
body,
pre {
margin: 0;
padding: 0;
}
code {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
max-width: 100%;
max-height: 100%;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo,
monospace;
}
</style>
</head>
<body>
<pre><code>{{ contents }}</code></pre>
<script
src="js/highlight.min.js"></script>
{{ languages }}
<script>
hljs.initHighlightingOnLoad();
</script>
</body>
</html>

@ -1,385 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="css/spectre.min.css"/>
<link rel="stylesheet" href="css/spectre-icons.min.css"/>
<style>
div[id$="-form"]:not(.active) {
display: none;
}
</style>
<title>tcp.ac</title>
<pre style="text-align: center; font-weight: bold;">
,d
88
MM88MMM ,adPPYba, 8b,dPPYba, ,adPPYYba, ,adPPYba,
88 a8" "" 88P' "8a "" `Y8 a8" ""
88 8b 88 d8 ,adPPPPP88 8b
88, "8a, ,aa 88b, ,a8" 888 88, ,88 "8a, ,aa
"Y888 `"Ybbd8"' 88`YbbdP"' 888 `"8bbdP"Y8 `"Ybbd8"'
88
88
</pre>
</head>
<body>
<nav class="container mb-2 pb-2">
<div class="columns">
<div
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto"
>
<ul class="tab tab-block">
<li id="img-tab" class="tab-item active">
<a href="#">img</a>
</li>
<li id="url-tab" class="tab-item" style="display:none;">
<a href="#">txt</a>
</li>
<li id="url-tab" class="tab-item" style="display:none;">
<a href="#">url</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container mt-2 pt-2">
<div class="columns">
<div
id="img-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto active">
<div class="form-group">
<label class="form-label" for="img-file">img</label>
<input
class="form-input"
id="img-file"
type="file"
required/>
<button
class="btn btn-primary input-group-btn"
id="img-submit"
disabled>
<i class="icon icon-upload"></i>
</button>
<p class="form-input-hint">accepted: jpeg,png,gif</p>
</div>
</div>
<div
id="url-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto">
<div class="form-group">
<label class="form-label" for="url-url">url</label>
<div class="input-group">
<span class="input-group-addon">/l/</span>
<input
id="url-url"
class="form-input"
type="text"
placeholder="404040"
required
/>
<button
class="btn btn-primary input-group-btn"
id="url-submit"
disabled
>
<i class="icon icon-upload"></i>
</button>
</div>
</div>
<div class="form-group">
<label class="form-label" for="url-forward"
>Forward</label
>
<input
id="url-forward"
class="form-input"
type="url"
placeholder="http://legitwebsite.cool/goatse.png"
required
/>
</div>
</div>
<div
id="txt-form"
class="column col-sm-12 col-md-10 col-lg-8 col-6 col-mx-auto"
>
<div class="form-group">
<label class="form-label" for="txt-url">url</label>
<div class="input-group">
<span class="input-group-addon">/t/</span>
<input
id="txt-url"
class="form-input"
type="text"
placeholder="a1b2c3"
required
/>
<button
class="btn btn-primary input-group-btn"
id="txt-submit"
disabled
>
<i class="icon icon-upload"></i>
</button>
</div>
</div>
<div class="form-group">
<label class="form-label" for="txt-contents"
>Contents</label
>
<textarea
id="txt-contents"
class="form-input"
placeholder="rm -rf /* --no-preserve-root"
required
></textarea>
</div>
<div class="form-group">
<label class="form-switch">
<input id="txt-highlight" type="checkbox" />
<i class="form-icon"></i> syntax highlighting
</label>
</div>
</div>
</div>
</main>
<div id="modal" class="modal">
<a id="modal-bg" href="#" class="modal-overlay"></a>
<div class="modal-container">
<div class="modal-header">
<div class="modal-title h6">success</div>
</div>
<div class="modal-body">
<div class="content">
<div class="form-group">
<div class="has-icon-right">
<input
id="modal-input"
type="url"
class="form-input"
/>
<i class="form-icon icon icon-copy"></i>
</div>
<p class="form-input-hint" id="modal-hint">
click to copy to clipboard
</p>
</div>
</div>
</div>
</div>
</div>
<script>
const tabs = {
img: [
document.querySelector("#img-tab"),
document.querySelector("#img-form"),
],
/*
url: [
document.querySelector("#url-tab"),
document.querySelector("#url-form"),
],
txt: [
document.querySelector("#txt-tab"),
document.querySelector("#txt-form"),
],
*/
};
const inputs = {
img: [
document.querySelector("#img-file"),
document.querySelector("#img-submit"),
],
/*
url: [
document.querySelector("#url-url"),
document.querySelector("#url-forward"),
document.querySelector("#url-submit"),
],
txt: [
document.querySelector("#txt-url"),
document.querySelector("#txt-contents"),
document.querySelector("#txt-highlight"),
document.querySelector("#txt-submit"),
],
*/
};
const used = {
img: [],
url: [],
txt: [],
};
let baseurl = `${location.protocol}//${location.host}${location.pathname}`;
if (!baseurl.endsWith("/")) {
baseurl += "/";
}
const modal = {
self: document.querySelector("#modal"),
input: document.querySelector("#modal-input"),
bg: document.querySelector("#modal-bg"),
hint: document.querySelector("#modal-hint"),
};
const openModal = (text) => {
modal.input.value = text;
modal.hint.innerText = "click to copy to clipboard";
modal.self.classList.add("active");
};
const closeModal = () => {
modal.hint.innerText = "copied to clipboard";
setTimeout(() => {
modal.self.classList.remove("active");
modal.input.value = "";
}, 1000);
};
modal.input.onclick = (e) => {
e.preventDefault();
modal.input.select();
document.execCommand("copy");
closeModal();
};
modal.bg.onclick = closeModal;
const fetchUsed = () => {
fetch(`${baseurl}i`)
.then((response) => response.json())
.then((json) => (used.img = json));
/*
fetch(`${baseurl}u`)
.then((response) => response.json())
.then((json) => (used.url = json));
fetch(`${baseurl}t`)
.then((response) => response.json())
.then((json) => (used.txt = json));
*/
};
fetchUsed();
/*
for (const group in tabs) {
tabs[group][0].onclick = () => {
const active = document.querySelectorAll(".active");
for (const el of active) {
el.classList.remove("active");
}
for (const el of tabs[group]) {
el.classList.add("active");
}
};
}
*/
const group = "img";
const submitButton = inputs[group][inputs[group].length - 1];
if (group === "img") {
submitButton.addEventListener("click", () => {
const imgFileInput = inputs.img[1];
const file = imgFileInput.img[0];
if (!file) {
alert(new Error("select a file first"));
return;
}
const fd = new FormData();
fd.append("upload", file);
let status;
fetch(url, {
method: "pUT",
body: fd,
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
/*
} else if (group === "url") {
submitButton.addEventListener("click", () => {
const id = urlInput.value;
const forward = inputs.url[1].value;
const url = `${baseurl}l/${id}`;
let status;
fetch(url, {
method: "pUT",
body: JSON.stringify({ forward }),
headers: { "content-Type": "application/json" },
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
} else if (group === "txt") {
submitButton.addEventListener("click", () => {
const id = urlInput.value;
const contents = inputs.txt[1].value;
const highlight = inputs.txt[2].checked;
const url = `${baseurl}t/${id}`;
let status;
fetch(url, {
method: "pUT",
body: JSON.stringify({ contents, highlight }),
headers: { "content-Type": "application/json" },
})
.then((response) => {
status = response.status;
return response.text();
})
.then((text) => {
if (status !== 201) {
throw new Error(text);
} else {
openModal(url);
clearInputs();
fetchUsed();
}
})
.catch((error) => alert(error));
});
}
*/
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long