-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: hmoazzem <[email protected]>
- Loading branch information
Showing
4 changed files
with
213 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package main | ||
|
||
import ( | ||
"embed" | ||
"flag" | ||
"fmt" | ||
"log" | ||
"net/http" | ||
|
||
mw "github.com/edgeflare/pgo/middleware" | ||
) | ||
|
||
// Embed the static directory | ||
// | ||
//go:embed dist/* | ||
var embeddedFS embed.FS | ||
|
||
var ( | ||
port = flag.Int("port", 8080, "port to listen on") | ||
directory = flag.String("dir", "dist", "directory to serve files from") | ||
spaFallback = flag.Bool("spa", false, "fallback to index.html for not-found files") | ||
useEmbedded = flag.Bool("embed", false, "use embedded static files") | ||
) | ||
|
||
func main() { | ||
flag.Parse() | ||
|
||
mux := http.NewServeMux() | ||
|
||
// other mux handlers | ||
// | ||
// static handler should be the last handler to catch all other requests | ||
if *useEmbedded { | ||
// Serve embedded static files | ||
mux.Handle("/", mw.ServeEmbedded(embeddedFS, *directory, *spaFallback)) | ||
} else { | ||
// Serve static files from a directory | ||
mux.Handle("/", mw.ServeStatic(*directory, *spaFallback)) | ||
} | ||
|
||
// // or mount on a `/different/path` other than root `/` | ||
// mux.Handle("/static/", http.StripPrefix("/static", mw.ServeStatic(*directory, *spaFallback))) | ||
|
||
// Start the server | ||
log.Printf("Starting server on :%d", *port) | ||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), mux)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package middleware | ||
|
||
import ( | ||
"bytes" | ||
"embed" | ||
"io/fs" | ||
"mime" | ||
"net/http" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"log" | ||
) | ||
|
||
// ServeEmbedded returns an http.Handler that serves files from an embedded filesystem. | ||
// It handles requests by attempting to read the requested file from the embedded filesystem. | ||
// If the file is not found, it can optionally serve a fallback index.html file for SPA (Single Page Application) support. | ||
// | ||
// Parameters: | ||
// - embeddedFS: The embedded filesystem containing the static files. | ||
// - baseDir: The base directory within the embedded filesystem where the files are located. | ||
// - spaFallback: A boolean indicating whether to serve index.html as a fallback when a file is not found. | ||
// | ||
// Returns: | ||
// - An http.Handler that serves files from the specified embedded filesystem. | ||
// | ||
// Example usage: | ||
// | ||
// mux := http.NewServeMux() | ||
// mux.Handle("GET /", ServeEmbedded(embeddedFS, "dist", true)) | ||
// | ||
// This will serve files from the "dist" directory of the embedded file system and use "index.html" | ||
// as a fallback for routes not directly mapped to a file. | ||
func ServeEmbedded(embeddedFS embed.FS, baseDir string, spaFallback bool) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
filePath := filepath.Join(baseDir, r.URL.Path) | ||
if !isValidFilePath(filePath) { | ||
http.Error(w, "Forbidden", http.StatusForbidden) | ||
return | ||
} | ||
|
||
fileData, fileInfo, err := readFileFromFS(embeddedFS, filePath) | ||
if err != nil && spaFallback { | ||
filePath = filepath.Join(baseDir, "index.html") | ||
fileData, fileInfo, err = readFileFromFS(embeddedFS, filePath) | ||
} | ||
|
||
if err != nil { | ||
http.NotFound(w, r) | ||
return | ||
} | ||
|
||
setContentType(w, filePath) | ||
http.ServeContent(w, r, filePath, fileInfo.ModTime(), bytes.NewReader(fileData)) | ||
}) | ||
} | ||
|
||
// ServeStatic returns an http.Handler that serves static files from the given directory. | ||
func ServeStatic(directory string, spaFallback bool) http.Handler { | ||
absDir, err := filepath.Abs(directory) | ||
if err != nil { | ||
log.Fatalf("Failed to resolve absolute path for %s: %v", directory, err) | ||
} | ||
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
path := strings.TrimPrefix(r.URL.Path, "/") | ||
cleanPath := filepath.Clean(path) | ||
|
||
requestedPath := filepath.Join(absDir, cleanPath) | ||
if !isSubPath(absDir, requestedPath) { | ||
http.Error(w, "Forbidden", http.StatusForbidden) | ||
return | ||
} | ||
|
||
fileInfo, err := os.Stat(requestedPath) | ||
if err != nil && spaFallback { | ||
indexPath := filepath.Join(absDir, "index.html") | ||
requestedPath = indexPath | ||
fileInfo, err = os.Stat(indexPath) | ||
} | ||
|
||
if err != nil { | ||
http.NotFound(w, r) | ||
return | ||
} | ||
|
||
if fileInfo.IsDir() && !spaFallback { | ||
http.Error(w, "Directory listing not allowed", http.StatusForbidden) | ||
return | ||
} | ||
|
||
setContentType(w, requestedPath) | ||
http.ServeFile(w, r, requestedPath) | ||
}) | ||
} | ||
|
||
// isValidFilePath checks if the file path is valid and does not contain any directory traversal attempts. | ||
func isValidFilePath(filePath string) bool { | ||
cleaned := filepath.Clean(filePath) | ||
if !strings.HasPrefix(cleaned, ".") { | ||
return true | ||
} else { | ||
return false | ||
} | ||
} | ||
|
||
// readFileFromFS reads a file from the embedded filesystem and returns its data and FileInfo. | ||
func readFileFromFS(embeddedFS embed.FS, filePath string) ([]byte, fs.FileInfo, error) { | ||
fileData, err := fs.ReadFile(embeddedFS, filePath) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
fileInfo, err := fs.Stat(embeddedFS, filePath) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
|
||
return fileData, fileInfo, nil | ||
} | ||
|
||
// isSubPath checks if a path is a subdirectory of the base directory. | ||
func isSubPath(baseDir, path string) bool { | ||
rel, err := filepath.Rel(baseDir, path) | ||
return err == nil && !strings.HasPrefix(rel, "..") | ||
} | ||
|
||
// setContentType sets the Content-Type header based on the file extension. | ||
func setContentType(w http.ResponseWriter, filePath string) { | ||
if ext := filepath.Ext(filePath); ext != "" { | ||
if ct := mime.TypeByExtension(ext); ct != "" { | ||
w.Header().Set("Content-Type", ct) | ||
} | ||
} | ||
} |