Initial commit of library

This commit is contained in:
dtookey 2022-07-18 12:17:03 -04:00
commit 35c87e82d4
8 changed files with 718 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea/
/vinegar-server.iml

6
go.mod Normal file
View File

@ -0,0 +1,6 @@
module vinegar-server
require (
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
google.golang.org/api v0.30.0
)
go 1.18

211
vinegar/WebLRU.go Normal file
View File

@ -0,0 +1,211 @@
package vinegar
import (
"bytes"
gzip2 "compress/gzip"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
)
const (
DefaultCacheTimeInMinutes = 15
ContentTypeHeaderKey = "Content-Type"
ContentEncodingHeaderKey = "Content-Encoding"
)
type (
lru map[string]*LruEntry
Lru struct {
entries *lru
limit int64
currentSize int64
}
LruEntry struct {
Content []byte
CompressedContent []byte
created time.Time
expires time.Time
mostRecentAccess time.Time
totalSize int64
MimeType string
}
SingleFileCache struct {
Content *[]byte
CompressedContent *[]byte
timeCreated time.Time
timeExpires time.Time
Mimetype string
}
)
func NewLRU(size int64) *Lru {
cLru := make(lru)
return &Lru{&cLru, size, 0}
}
func NewSingleFileCache(pathlike string) *SingleFileCache {
bits, exists := GetDiskContent(pathlike)
if !exists {
panic("Could not load single file for single file path: " + pathlike)
}
compressedBits := GZipBytes(bits)
cache := SingleFileCache{
Content: bits,
CompressedContent: compressedBits,
timeCreated: time.Now(),
timeExpires: time.Now().Add(DefaultCacheTimeInMinutes),
Mimetype: GuessMimetype(pathlike),
}
return &cache
}
func (l *Lru) Put(key string, content *[]byte) {
entry, exists := (*l.entries)[key]
zippedContent := *GZipBytes(content)
size := int64(len(*content)) + int64(len(zippedContent))
l.ensureVacancy(size)
if exists {
entry.Content = *content
entry.CompressedContent = zippedContent
entry.created = time.Now()
entry.expires = time.Now().Add(DefaultCacheTimeInMinutes * time.Minute)
entry.mostRecentAccess = time.Now()
entry.totalSize = size
} else {
mimetype := GuessMimetype(key)
entry = &LruEntry{
Content: *content,
CompressedContent: zippedContent,
created: time.Now(),
expires: time.Now().Add(DefaultCacheTimeInMinutes * time.Minute),
mostRecentAccess: time.Now(),
totalSize: size,
MimeType: mimetype,
}
}
(*l.entries)[key] = entry
l.currentSize = l.currentSize + size
}
func (l *Lru) Get(key string) (*LruEntry, bool) {
entry, exists := (*l.entries)[key]
if exists && entry.expires.Before(time.Now()) {
fmt.Println("Cache miss due to expired content")
l.Remove(key)
return nil, false
} else {
if exists {
entry.mostRecentAccess = time.Now()
} else {
fmt.Printf("cache miss for '%s'\n", key)
}
return entry, exists
}
}
func (l *Lru) Remove(key string) {
entry, exists := (*l.entries)[key]
if exists {
size := entry.totalSize
delete(*l.entries, key)
l.currentSize = l.currentSize - size
}
}
func (l *Lru) ensureVacancy(required int64) {
if l.currentSize == 0 {
return
}
for l.currentSize+required > l.limit {
leastKey := l.getLeastRecentlyUsedKey()
l.Remove(leastKey)
}
}
func (l *Lru) getLeastRecentlyUsedKey() string {
started := false
var benchmark *LruEntry
leastUsedKey := ""
for key, entry := range *l.entries {
if !started {
started = true
benchmark = entry
leastUsedKey = key
continue
}
if benchmark.mostRecentAccess.After(entry.mostRecentAccess) {
benchmark = entry
}
}
if leastUsedKey == "" {
panic("Invalid key for LRU: [" + leastUsedKey + "]")
}
return leastUsedKey
}
func GZipBytes(uncompressed *[]byte) *[]byte {
buff := bytes.Buffer{}
gzip := gzip2.NewWriter(&buff)
_, err := gzip.Write(*uncompressed)
if err != nil {
panic(err)
}
err = gzip.Flush()
if err != nil {
panic(err)
}
compressed := buff.Bytes()
return &compressed
}
func GuessMimetype(filePath string) string {
dotIndex := strings.LastIndex(filePath, ".")
ext := filePath[dotIndex+1:]
switch ext {
case "htm":
case "html":
return "text/html"
case "ico":
return "image/vnd.microsoft.icon"
case "jpg":
case "jpeg":
return "image/jpeg"
case "png":
return "image/png"
case "svg":
return "image/svg+xml"
default:
return "application/octet-stream"
}
return "application/octet-stream"
}
func GetDiskContent(filePath string) (*[]byte, bool) {
_, ferr := os.Stat(filePath)
if os.IsNotExist(ferr) {
return nil, false
} else {
f, err := os.OpenFile(filePath, os.O_RDONLY, 0755)
if err != nil {
panic(err)
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
return &content, true
}
}

9
vinegar/campaign.go Normal file
View File

@ -0,0 +1,9 @@
package vinegar
type Campaign struct {
Targets *[]string
HostName string
ListeningPort int
WebRoot string
EmailTemplate string
}

86
vinegar/dynamicRoute.go Normal file
View File

@ -0,0 +1,86 @@
package vinegar
import (
"fmt"
"net/http"
)
type (
ApiRoute struct {
ServletRoute *ServletRoute
HttpMethodRoutes *map[HttpMethod]ServletHandleFunction
}
)
type (
HttpMethod int
)
const (
GET HttpMethod = iota
POST
PUT
PATCH
DELETE
UNDEFINED
)
func NewApiRoute(pattern string) *ApiRoute {
functionMap := make(map[HttpMethod]ServletHandleFunction)
ancestorRoute := NewServletRoute(pattern, createMethodHandler(&functionMap))
route := ApiRoute{
ancestorRoute,
&functionMap,
}
return &route
}
func createMethodHandler(m *map[HttpMethod]ServletHandleFunction) ServletHandleFunction {
return func(w http.ResponseWriter, req *http.Request) {
method := getHttpMethod(req)
fn, exists := (*m)[method]
if exists {
fn(w, req)
} else {
SendApiError(
w,
200,
404,
"Method ["+req.Method+"] does not exist for endpoint["+req.URL.Path+"].",
)
}
}
}
func (api *ApiRoute) RegisterHttpMethodHandler(method HttpMethod, handler ServletHandleFunction) {
(*api.HttpMethodRoutes)[method] = handler
}
func getHttpMethod(req *http.Request) HttpMethod {
switch req.Method {
case "GET":
return GET
case "POST":
return POST
case "PUT":
return PUT
case "PATCH":
return PATCH
case "DELETE":
return DELETE
case "UNDEFINED":
default:
return UNDEFINED
}
return UNDEFINED
}
func SendApiError(w http.ResponseWriter, httpCode int, messageCode int, message string) {
respMessage := fmt.Sprintf("{\"code\":%d, \"message\":\"%s\"}", messageCode, message)
w.WriteHeader(httpCode)
_, err := w.Write([]byte(respMessage))
if err != nil {
panic(err)
}
}

161
vinegar/email.go Normal file
View File

@ -0,0 +1,161 @@
package vinegar
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
"io/ioutil"
"log"
"net/http"
"os"
)
//<editor-fold name="secure gmail">
/*======================================================================================
secure gmail
======================================================================================*/
// Retrieve a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config) *http.Client {
// The file token.json stores the user's access and refresh tokens, and is
// created automatically when the authorization flow completes for the first
// time.
tokFile := "token.json"
tok, err := tokenFromFile(tokFile)
if err != nil {
tok = getTokenFromWeb(config)
saveToken(tokFile, tok)
}
return config.Client(context.Background(), tok)
}
// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
fmt.Printf("Go to the following link in your browser then type the "+
"authorization code: \n%v\n", authURL)
var authCode string
if _, err := fmt.Scan(&authCode); err != nil {
log.Fatalf("Unable to read authorization code: %v", err)
}
tok, err := config.Exchange(context.TODO(), authCode)
if err != nil {
log.Fatalf("Unable to retrieve token from web: %v", err)
}
return tok
}
// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
tok := &oauth2.Token{}
err = json.NewDecoder(f).Decode(tok)
return tok, err
}
// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
fmt.Printf("Saving credential file to: %s\n", path)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Unable to cache oauth token: %v", err)
}
defer f.Close()
json.NewEncoder(f).Encode(token)
}
func getMailService() *gmail.Service {
ctx := context.Background()
b, err := ioutil.ReadFile("gmail-creds.json")
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
// If modifying these scopes, delete your previously saved token.json.
config, err := google.ConfigFromJSON(b, gmail.GmailSendScope, gmail.GmailComposeScope)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
client := getClient(config)
srv, err := gmail.NewService(ctx, option.WithHTTPClient(client))
if err != nil {
log.Fatalf("Unable to retrieve Gmail client: %v", err)
}
return srv
}
func TestSend() {
srv := getMailService()
payload := []byte("testing")
msg := gmail.Message{Raw: base64.URLEncoding.EncodeToString(payload)}
msgResp, err := srv.Users.Messages.Send("me", &msg).Do()
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", msgResp)
}
//</editor-fold>

125
vinegar/server.go Normal file
View File

@ -0,0 +1,125 @@
package vinegar
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"regexp"
)
const (
defaultLruSize = int64(1024 * 1024 * 50)
)
type ErrorResponse struct {
Code int
Message string
}
type (
Servlet struct {
Port string
Routes []*ServletRoute
Cache *Lru
}
ServletRoute struct {
Pattern *regexp.Regexp
Handler ServletHandleFunction
}
ServletHandleFunction func(w http.ResponseWriter, req *http.Request)
)
func NewServlet(port string) *Servlet {
lru := NewLRU(defaultLruSize)
srv := Servlet{Port: port, Cache: lru}
return &srv
}
func NewServletRoute(routePattern string, handleFunc ServletHandleFunction) *ServletRoute {
route := ServletRoute{}
pattern := regexp.MustCompile(routePattern)
route.Pattern = pattern
route.Handler = handleFunc
return &route
}
func (s *Servlet) AddRoute(route *ServletRoute) {
s.Routes = append(s.Routes, route)
}
func (s *Servlet) ServeHTTP(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
fmt.Println("Attempting to match request for " + path)
for _, route := range s.Routes {
if route.Pattern.MatchString(path) {
route.Handler(w, req)
return
}
}
}
func StartServerGood(s *Servlet) {
err := http.ListenAndServe(s.Port, s)
if err != nil {
panic(err)
}
}
func SendError(w http.ResponseWriter, code int, msg string) {
errorFile := "templates/error.html"
errorResp := ErrorResponse{code, msg}
tmplManager := template.New("error")
f, err := os.OpenFile(errorFile, os.O_RDONLY, 0777)
if err != nil {
panic(err)
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
tmpl, err := tmplManager.Parse(string(content))
if err != nil {
panic(err)
}
w.WriteHeader(code)
err = tmpl.Execute(w, errorResp)
if err != nil {
panic(err)
}
}
func renderTemplate(w http.ResponseWriter, pathlike string, data any) {
templateHelper := template.New(pathlike)
f, err := os.OpenFile(pathlike, os.O_RDONLY, 0777)
if err != nil {
panic(err)
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
panic(err)
}
templ, err := templateHelper.Parse(string(content))
err = templ.Execute(w, data)
if err != nil {
panic(err)
}
}

118
vinegar/staticRoute.go Normal file
View File

@ -0,0 +1,118 @@
package vinegar
import (
"net/http"
"path"
"strings"
)
type (
FileRoute struct {
*ServletRoute
fileRoot string
}
)
func NewImageRoute(urlPattern string, pathlike string, servlet *Servlet) *FileRoute {
defaultPrune := strings.Replace(urlPattern, ".*", "", -1)
rootRoute := NewServletRoute(urlPattern, createUncompressedFileServletFunction(defaultPrune, pathlike, servlet.Cache))
imgRoute := FileRoute{rootRoute, pathlike}
return &imgRoute
}
func NewTextRoute(urlPattern string, pathlike string, servlet *Servlet) *FileRoute {
defaultPrune := strings.Replace(urlPattern, ".*", "", -1)
rootRoute := NewServletRoute(urlPattern, createCompressibleFileServletFunction(defaultPrune, pathlike, servlet.Cache))
fr := FileRoute{rootRoute, pathlike}
return &fr
}
func NewSingleFileRoute(urlPattern string, pathlike string, servlet *Servlet) *FileRoute {
fun := createSingleFileServletFunction(pathlike)
route := FileRoute{
ServletRoute: NewServletRoute("^"+urlPattern+"$", fun),
fileRoot: pathlike,
}
return &route
}
func createSingleFileServletFunction(pathlike string) ServletHandleFunction {
cache := NewSingleFileCache(pathlike)
var fun ServletHandleFunction = func(w http.ResponseWriter, req *http.Request) {
w.Header().Add(ContentTypeHeaderKey, cache.Mimetype)
if clientAcceptsGzip(req) {
w.Header().Add(ContentEncodingHeaderKey, "gzip")
w.Write(*cache.CompressedContent)
}
}
return fun
}
func createCompressibleFileServletFunction(basePattern string, pathlike string, cache *Lru) ServletHandleFunction {
var fun ServletHandleFunction = func(w http.ResponseWriter, req *http.Request) {
stub := strings.Replace(req.URL.Path, basePattern, "", 1)
cachedContent, exists := cache.Get(stub)
if !exists {
content, fileExists := GetDiskContent(path.Join(pathlike, stub))
if fileExists {
cache.Put(stub, content)
cachedContent, _ = cache.Get(stub)
} else {
SendError(w, 404, "Oops! Something went wrong.")
return
}
}
w.Header().Add(ContentTypeHeaderKey, cachedContent.MimeType)
var err error = nil
if clientAcceptsGzip(req) {
w.Header().Add(ContentEncodingHeaderKey, "gzip")
_, err = w.Write(cachedContent.CompressedContent)
} else {
_, err = w.Write(cachedContent.Content)
}
if err != nil {
panic(err)
}
}
return fun
}
func createUncompressedFileServletFunction(basePattern string, pathlike string, cache *Lru) ServletHandleFunction {
var fun ServletHandleFunction = func(w http.ResponseWriter, req *http.Request) {
stub := strings.Replace(req.URL.Path, basePattern, "", 1)
entry, exists := cache.Get(stub)
if !exists {
fileContent, fExists := GetDiskContent(path.Join(pathlike, stub))
if fExists {
cache.Put(stub, fileContent)
entry, exists = cache.Get(stub)
} else {
SendError(w, 404, "Oops! Something went wrong!")
return
}
}
if !exists {
SendError(w, 404, "Oops! Something went wrong.")
} else {
_, err := w.Write(entry.Content)
if err != nil {
panic(err)
}
return
}
}
return fun
}
func clientAcceptsGzip(req *http.Request) bool {
encodings := req.Header.Get("Accept-Encoding")
if strings.Contains(encodings, "gzip") {
return true
}
return false
}