354 lines
13 KiB
Go
354 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"runtime"
|
|
d "runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/h2non/bimg"
|
|
)
|
|
|
|
var (
|
|
aAddr = flag.String("a", "", "Bind address")
|
|
aPort = flag.Int("p", 8088, "Port to listen")
|
|
aVers = flag.Bool("v", false, "Show version")
|
|
aVersl = flag.Bool("version", false, "Show version")
|
|
aHelp = flag.Bool("h", false, "Show help")
|
|
aHelpl = flag.Bool("help", false, "Show help")
|
|
aPathPrefix = flag.String("path-prefix", "/", "Url path prefix to listen to")
|
|
aCors = flag.Bool("cors", false, "Enable CORS support")
|
|
aGzip = flag.Bool("gzip", false, "Enable gzip compression (deprecated)")
|
|
aAuthForwarding = flag.Bool("enable-auth-forwarding", false, "Forwards X-Forward-Authorization or Authorization header to the image source server. -enable-url-source flag must be defined. Tip: secure your server from public access to prevent attack vectors")
|
|
aEnableURLSource = flag.Bool("enable-url-source", false, "Enable remote HTTP URL image source processing")
|
|
aEnablePlaceholder = flag.Bool("enable-placeholder", false, "Enable image response placeholder to be used in case of error")
|
|
aEnableURLSignature = flag.Bool("enable-url-signature", false, "Enable URL signature (URL-safe Base64-encoded HMAC digest)")
|
|
aURLSignatureKey = flag.String("url-signature-key", "", "The URL signature key (32 characters minimum)")
|
|
aAllowedOrigins = flag.String("allowed-origins", "", "Restrict remote image source processing to certain origins (separated by commas). Note: Origins are validated against host *AND* path.")
|
|
aMaxAllowedSize = flag.Int("max-allowed-size", 0, "Restrict maximum size of http image source (in bytes)")
|
|
aKey = flag.String("key", "", "Define API key for authorization")
|
|
aMount = flag.String("mount", "", "Mount server local directory")
|
|
aCertFile = flag.String("certfile", "", "TLS certificate file path")
|
|
aKeyFile = flag.String("keyfile", "", "TLS private key file path")
|
|
aAuthorization = flag.String("authorization", "", "Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization")
|
|
aForwardHeaders = flag.String("forward-headers", "", "Forwards custom headers to the image source server. -enable-url-source flag must be defined.")
|
|
aPlaceholder = flag.String("placeholder", "", "Image path to image custom placeholder to be used in case of error. Recommended minimum image size is: 1200x1200")
|
|
aPlaceholderStatus = flag.Int("placeholder-status", 0, "HTTP status returned when use -placeholder flag")
|
|
aDisableEndpoints = flag.String("disable-endpoints", "", "Comma separated endpoints to disable. E.g: form,crop,rotate,health")
|
|
aHTTPCacheTTL = flag.Int("http-cache-ttl", -1, "The TTL in seconds")
|
|
aReadTimeout = flag.Int("http-read-timeout", 60, "HTTP read timeout in seconds")
|
|
aWriteTimeout = flag.Int("http-write-timeout", 60, "HTTP write timeout in seconds")
|
|
aConcurrency = flag.Int("concurrency", 0, "Throttle concurrency limit per second")
|
|
aBurst = flag.Int("burst", 100, "Throttle burst max cache size")
|
|
aMRelease = flag.Int("mrelease", 30, "OS memory release interval in seconds")
|
|
aCpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "Number of cpu cores to use")
|
|
aLogLevel = flag.String("log-level", "info", "Define log level for http-server. E.g: info,warning,error")
|
|
)
|
|
|
|
const usage = `imaginary %s
|
|
|
|
Usage:
|
|
imaginary -p 80
|
|
imaginary -cors
|
|
imaginary -concurrency 10
|
|
imaginary -path-prefix /api/v1
|
|
imaginary -enable-url-source
|
|
imaginary -disable-endpoints form,health,crop,rotate
|
|
imaginary -enable-url-source -allowed-origins http://localhost,http://server.com
|
|
imaginary -enable-url-source -enable-auth-forwarding
|
|
imaginary -enable-url-source -authorization "Basic AwDJdL2DbwrD=="
|
|
imaginary -enable-placeholder
|
|
imaginary -enable-url-source -placeholder ./placeholder.jpg
|
|
imaginary -enable-url-signature -url-signature-key 4f46feebafc4b5e988f131c4ff8b5997
|
|
imaginary -enable-url-source -forward-headers X-Custom,X-Token
|
|
imaginary -h | -help
|
|
imaginary -v | -version
|
|
|
|
Options:
|
|
|
|
-a <addr> Bind address [default: *]
|
|
-p <port> Bind port [default: 8088]
|
|
-h, -help Show help
|
|
-v, -version Show version
|
|
-path-prefix <value> Url path prefix to listen to [default: "/"]
|
|
-cors Enable CORS support [default: false]
|
|
-gzip Enable gzip compression (deprecated) [default: false]
|
|
-disable-endpoints Comma separated endpoints to disable. E.g: form,crop,rotate,health [default: ""]
|
|
-key <key> Define API key for authorization
|
|
-mount <path> Mount server local directory
|
|
-http-cache-ttl <num> The TTL in seconds. Adds caching headers to locally served files.
|
|
-http-read-timeout <num> HTTP read timeout in seconds [default: 30]
|
|
-http-write-timeout <num> HTTP write timeout in seconds [default: 30]
|
|
-enable-url-source Enable remote HTTP URL image source processing
|
|
-enable-placeholder Enable image response placeholder to be used in case of error [default: false]
|
|
-enable-auth-forwarding Forwards X-Forward-Authorization or Authorization header to the image source server. -enable-url-source flag must be defined. Tip: secure your server from public access to prevent attack vectors
|
|
-forward-headers Forwards custom headers to the image source server. -enable-url-source flag must be defined.
|
|
-enable-url-signature Enable URL signature (URL-safe Base64-encoded HMAC digest) [default: false]
|
|
-url-signature-key The URL signature key (32 characters minimum)
|
|
-allowed-origins <urls> Restrict remote image source processing to certain origins (separated by commas)
|
|
-max-allowed-size <bytes> Restrict maximum size of http image source (in bytes)
|
|
-certfile <path> TLS certificate file path
|
|
-keyfile <path> TLS private key file path
|
|
-authorization <value> Defines a constant Authorization header value passed to all the image source servers. -enable-url-source flag must be defined. This overwrites authorization headers forwarding behavior via X-Forward-Authorization
|
|
-placeholder <path> Image path to image custom placeholder to be used in case of error. Recommended minimum image size is: 1200x1200
|
|
-placeholder-status <code> HTTP status returned when use -placeholder flag
|
|
-concurrency <num> Throttle concurrency limit per second [default: disabled]
|
|
-burst <num> Throttle burst max cache size [default: 100]
|
|
-mrelease <num> OS memory release interval in seconds [default: 30]
|
|
-cpus <num> Number of used cpu cores.
|
|
(default for current machine is %d cores)
|
|
-log-level Set log level for http-server. E.g: info,warning,error [default: info].
|
|
Or can use the environment variable GOLANG_LOG=info.
|
|
`
|
|
|
|
type URLSignature struct {
|
|
Key string
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
_, _ = fmt.Fprintf(os.Stderr, usage, Version, runtime.NumCPU())
|
|
}
|
|
flag.Parse()
|
|
|
|
if *aHelp || *aHelpl {
|
|
showUsage()
|
|
}
|
|
if *aVers || *aVersl {
|
|
showVersion()
|
|
}
|
|
|
|
// Only required in Go < 1.5
|
|
runtime.GOMAXPROCS(*aCpus)
|
|
|
|
port := getPort(*aPort)
|
|
urlSignature := getURLSignature(*aURLSignatureKey)
|
|
|
|
opts := ServerOptions{
|
|
Port: port,
|
|
Address: *aAddr,
|
|
CORS: *aCors,
|
|
AuthForwarding: *aAuthForwarding,
|
|
EnableURLSource: *aEnableURLSource,
|
|
EnablePlaceholder: *aEnablePlaceholder,
|
|
EnableURLSignature: *aEnableURLSignature,
|
|
URLSignatureKey: urlSignature.Key,
|
|
PathPrefix: *aPathPrefix,
|
|
APIKey: *aKey,
|
|
Concurrency: *aConcurrency,
|
|
Burst: *aBurst,
|
|
Mount: *aMount,
|
|
CertFile: *aCertFile,
|
|
KeyFile: *aKeyFile,
|
|
Placeholder: *aPlaceholder,
|
|
PlaceholderStatus: *aPlaceholderStatus,
|
|
HTTPCacheTTL: *aHTTPCacheTTL,
|
|
HTTPReadTimeout: *aReadTimeout,
|
|
HTTPWriteTimeout: *aWriteTimeout,
|
|
Authorization: *aAuthorization,
|
|
ForwardHeaders: parseForwardHeaders(*aForwardHeaders),
|
|
AllowedOrigins: parseOrigins(*aAllowedOrigins),
|
|
MaxAllowedSize: *aMaxAllowedSize,
|
|
LogLevel: getLogLevel(*aLogLevel),
|
|
}
|
|
|
|
// Show warning if gzip flag is passed
|
|
if *aGzip {
|
|
fmt.Println("warning: -gzip flag is deprecated and will not have effect")
|
|
}
|
|
|
|
// Create a memory release goroutine
|
|
if *aMRelease > 0 {
|
|
memoryRelease(*aMRelease)
|
|
}
|
|
|
|
// Check if the mount directory exists, if present
|
|
if *aMount != "" {
|
|
checkMountDirectory(*aMount)
|
|
}
|
|
|
|
// Validate HTTP cache param, if present
|
|
if *aHTTPCacheTTL != -1 {
|
|
checkHTTPCacheTTL(*aHTTPCacheTTL)
|
|
}
|
|
|
|
// Parse endpoint names to disabled, if present
|
|
if *aDisableEndpoints != "" {
|
|
opts.Endpoints = parseEndpoints(*aDisableEndpoints)
|
|
}
|
|
|
|
// Read placeholder image, if required
|
|
if *aPlaceholder != "" {
|
|
buf, err := ioutil.ReadFile(*aPlaceholder)
|
|
if err != nil {
|
|
exitWithError("cannot start the server: %s", err)
|
|
}
|
|
|
|
imageType := bimg.DetermineImageType(buf)
|
|
if !bimg.IsImageTypeSupportedByVips(imageType).Load {
|
|
exitWithError("Placeholder image type is not supported. Only JPEG, PNG or WEBP are supported")
|
|
}
|
|
|
|
opts.PlaceholderImage = buf
|
|
} else if *aEnablePlaceholder {
|
|
// Expose default placeholder
|
|
opts.PlaceholderImage = placeholder
|
|
}
|
|
|
|
// Check URL signature key, if required
|
|
if *aEnableURLSignature {
|
|
if urlSignature.Key == "" {
|
|
exitWithError("URL signature key is required")
|
|
}
|
|
|
|
if len(urlSignature.Key) < 32 {
|
|
exitWithError("URL signature key must be a minimum of 32 characters")
|
|
}
|
|
}
|
|
|
|
debug("imaginary server listening on port :%d/%s", opts.Port, strings.TrimPrefix(opts.PathPrefix, "/"))
|
|
|
|
// Load image source providers
|
|
LoadSources(opts)
|
|
|
|
// Start the server
|
|
Server(opts)
|
|
}
|
|
|
|
func getPort(port int) int {
|
|
if portEnv := os.Getenv("PORT"); portEnv != "" {
|
|
newPort, _ := strconv.Atoi(portEnv)
|
|
if newPort > 0 {
|
|
port = newPort
|
|
}
|
|
}
|
|
return port
|
|
}
|
|
|
|
func getURLSignature(key string) URLSignature {
|
|
if keyEnv := os.Getenv("URL_SIGNATURE_KEY"); keyEnv != "" {
|
|
key = keyEnv
|
|
}
|
|
|
|
return URLSignature{key}
|
|
}
|
|
|
|
func getLogLevel(logLevel string) string {
|
|
if logLevelEnv := os.Getenv("GOLANG_LOG"); logLevelEnv != "" {
|
|
logLevel = logLevelEnv
|
|
}
|
|
return logLevel
|
|
}
|
|
|
|
func showUsage() {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
func showVersion() {
|
|
fmt.Println(Version)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func checkMountDirectory(path string) {
|
|
src, err := os.Stat(path)
|
|
if err != nil {
|
|
exitWithError("error while mounting directory: %s", err)
|
|
}
|
|
if !src.IsDir() {
|
|
exitWithError("mount path is not a directory: %s", path)
|
|
}
|
|
if path == "/" {
|
|
exitWithError("cannot mount root directory for security reasons")
|
|
}
|
|
}
|
|
|
|
func checkHTTPCacheTTL(ttl int) {
|
|
if ttl < 0 || ttl > 31556926 {
|
|
exitWithError("The -http-cache-ttl flag only accepts a value from 0 to 31556926")
|
|
}
|
|
|
|
if ttl == 0 {
|
|
debug("Adding HTTP cache control headers set to prevent caching.")
|
|
}
|
|
}
|
|
|
|
func parseForwardHeaders(forwardHeaders string) []string {
|
|
var headers []string
|
|
if forwardHeaders == "" {
|
|
return headers
|
|
}
|
|
|
|
for _, header := range strings.Split(forwardHeaders, ",") {
|
|
if norm := strings.TrimSpace(header); norm != "" {
|
|
headers = append(headers, norm)
|
|
}
|
|
}
|
|
return headers
|
|
}
|
|
|
|
func parseOrigins(origins string) []*url.URL {
|
|
var urls []*url.URL
|
|
if origins == "" {
|
|
return urls
|
|
}
|
|
for _, origin := range strings.Split(origins, ",") {
|
|
u, err := url.Parse(origin)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if u.Path != "" {
|
|
var lastChar = u.Path[len(u.Path)-1:]
|
|
if lastChar == "*" {
|
|
u.Path = strings.TrimSuffix(u.Path, "*")
|
|
} else if lastChar != "/" {
|
|
u.Path += "/"
|
|
}
|
|
}
|
|
|
|
urls = append(urls, u)
|
|
}
|
|
return urls
|
|
}
|
|
|
|
func parseEndpoints(input string) Endpoints {
|
|
var endpoints Endpoints
|
|
for _, endpoint := range strings.Split(input, ",") {
|
|
endpoint = strings.ToLower(strings.TrimSpace(endpoint))
|
|
if endpoint != "" {
|
|
endpoints = append(endpoints, endpoint)
|
|
}
|
|
}
|
|
return endpoints
|
|
}
|
|
|
|
func memoryRelease(interval int) {
|
|
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
|
go func() {
|
|
for range ticker.C {
|
|
debug("FreeOSMemory()")
|
|
d.FreeOSMemory()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func exitWithError(format string, args ...interface{}) {
|
|
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func debug(msg string, values ...interface{}) {
|
|
debug := os.Getenv("DEBUG")
|
|
if debug == "imaginary" || debug == "*" {
|
|
log.Printf(msg, values...)
|
|
}
|
|
}
|