From 35c87e82d424556f9e33447af877f4a68bc8332f Mon Sep 17 00:00:00 2001 From: dtookey Date: Mon, 18 Jul 2022 12:17:03 -0400 Subject: [PATCH] Initial commit of library --- .gitignore | 2 + go.mod | 6 ++ vinegar/WebLRU.go | 211 ++++++++++++++++++++++++++++++++++++++++ vinegar/campaign.go | 9 ++ vinegar/dynamicRoute.go | 86 ++++++++++++++++ vinegar/email.go | 161 ++++++++++++++++++++++++++++++ vinegar/server.go | 125 ++++++++++++++++++++++++ vinegar/staticRoute.go | 118 ++++++++++++++++++++++ 8 files changed, 718 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 vinegar/WebLRU.go create mode 100644 vinegar/campaign.go create mode 100644 vinegar/dynamicRoute.go create mode 100644 vinegar/email.go create mode 100644 vinegar/server.go create mode 100644 vinegar/staticRoute.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bc3fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/vinegar-server.iml diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5594234 --- /dev/null +++ b/go.mod @@ -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 diff --git a/vinegar/WebLRU.go b/vinegar/WebLRU.go new file mode 100644 index 0000000..75113be --- /dev/null +++ b/vinegar/WebLRU.go @@ -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 + } +} diff --git a/vinegar/campaign.go b/vinegar/campaign.go new file mode 100644 index 0000000..4b26306 --- /dev/null +++ b/vinegar/campaign.go @@ -0,0 +1,9 @@ +package vinegar + +type Campaign struct { + Targets *[]string + HostName string + ListeningPort int + WebRoot string + EmailTemplate string +} diff --git a/vinegar/dynamicRoute.go b/vinegar/dynamicRoute.go new file mode 100644 index 0000000..55ee143 --- /dev/null +++ b/vinegar/dynamicRoute.go @@ -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) + } +} diff --git a/vinegar/email.go b/vinegar/email.go new file mode 100644 index 0000000..1dbb22d --- /dev/null +++ b/vinegar/email.go @@ -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" +) + +// + +/*====================================================================================== + + + + 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) +} + +// diff --git a/vinegar/server.go b/vinegar/server.go new file mode 100644 index 0000000..a2174ef --- /dev/null +++ b/vinegar/server.go @@ -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) + } + +} diff --git a/vinegar/staticRoute.go b/vinegar/staticRoute.go new file mode 100644 index 0000000..2a8b134 --- /dev/null +++ b/vinegar/staticRoute.go @@ -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 +}