diff --git a/.gitignore b/.gitignore index 487c5548..d253d17e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ ui/static/application.min.js ui/dist/ *.swp -/imaginary +bin/imaginary ## ignore vendor packages vendor/ \ No newline at end of file diff --git a/Makefile b/Makefile index 8f686c36..be8884fe 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,10 @@ +OK_COLOR=\033[32;01m +NO_COLOR=\033[0m + +build: + @echo "$(OK_COLOR)==> Compiling binary$(NO_COLOR)" + go build -o bin/imaginary + test: go test diff --git a/controllers.go b/controllers.go index b6493ed0..367f2996 100644 --- a/controllers.go +++ b/controllers.go @@ -2,10 +2,9 @@ package main import ( "fmt" + "gopkg.in/h2non/bimg.v0" "net/http" "time" - - "gopkg.in/h2non/bimg.v0" ) func indexController(w http.ResponseWriter, r *http.Request) { @@ -22,6 +21,11 @@ func formController(w http.ResponseWriter, r *http.Request) { w.Write([]byte(htmlForm())) } +func healthController(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{}")) +} + func imageControllerDispatcher(o ServerOptions, operation Operation) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var buf []byte diff --git a/fixtures/server.crt b/fixtures/server.crt new file mode 100644 index 00000000..3a8d94d7 --- /dev/null +++ b/fixtures/server.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICjzCCAfgCCQCYQsNXSKvPPTANBgkqhkiG9w0BAQUFADCBizELMAkGA1UEBhMC +SUUxDzANBgNVBAgTBkR1YmxpbjEPMA0GA1UEBxMGRHVibGluMSEwHwYDVQQKExhJ +bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFTATBgNVBAMTDGltYWdpbmFyeS5pbzEg +MB4GCSqGSIb3DQEJARYRdG9tYXNAYXBhcmljaW8ubWUwHhcNMTUwNzExMTk0MTM2 +WhcNMjcwNjIzMTk0MTM2WjCBizELMAkGA1UEBhMCSUUxDzANBgNVBAgTBkR1Ymxp +bjEPMA0GA1UEBxMGRHVibGluMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0 +eSBMdGQxFTATBgNVBAMTDGltYWdpbmFyeS5pbzEgMB4GCSqGSIb3DQEJARYRdG9t +YXNAYXBhcmljaW8ubWUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKv846TF +luaJi9lSpQxk3lwfU32gCpaZHysGmAtEkYzLCWKXV212AcKzW6v07lKD7w7mJ7Fr +RTMNT++tcBDNL4RAVdyLhYhHIabmWOh85cPaWwM+6tE9JxlQKQi6qYE2P7sE4D9f +EjIGi7wnBOsXNHCWpQExmkY1g3GYiCsBa3QTAgMBAAEwDQYJKoZIhvcNAQEFBQAD +gYEAHxWFoEQh4/bzKc9ByGLdPubfRkck7mnA37leJO/ilooS7ZL22BW/yjzlP3dM +LzCMFmBBNqHwPQMfnFWqoIAaHFa6FPgCZExZZ+xYfRxfatnVI2t11lQXZOUe8Dxf +pQcjzecXFSMlhSXNQPYyZZzUjCUOgZDs0HSTvvRAStQjENU= +-----END CERTIFICATE----- diff --git a/fixtures/server.key b/fixtures/server.key new file mode 100644 index 00000000..48df66a0 --- /dev/null +++ b/fixtures/server.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCr/OOkxZbmiYvZUqUMZN5cH1N9oAqWmR8rBpgLRJGMywlil1dt +dgHCs1ur9O5Sg+8O5iexa0UzDU/vrXAQzS+EQFXci4WIRyGm5ljofOXD2lsDPurR +PScZUCkIuqmBNj+7BOA/XxIyBou8JwTrFzRwlqUBMZpGNYNxmIgrAWt0EwIDAQAB +AoGAXmSYiDmN3Y+GOst6HHhL9iGXUC6DQS5fBd1Dm4ORosVYrEzFxiTrSHHqEVGH +b7BLh1DYXi6ytxdKVRBKnl4PAk9NUsdWdSFSvOv6wUM/uLMpKLYEdPRHcaHNSIca +sz5ryRwA9zogS76Ke20tgPC6IklTVaEkbvfxG+ob7Vp53rECQQDacScoyDVsadlS +8oQqogO+XuTwgyk4UDd7K6KmMq0etS00SpPtlp1XvP90kqa+1OCAwjP8aUW8Bj+Q +hbNa6yD5AkEAyY8H12He86EXFatKyrIRaQ71aMtgTOQ4cnH0l8EYKuDpKLA8mUdT +skzu4EKxyt/6pg4yGwGzK+ruGH6J96EMawJAB82E7ZMBPYcmaS0ahX9WDOXM3b6B +qW5MHQ04+SDUSEWGgNitIg6APlMU+PAIHsbx4geN3dVQ1V+Pw7TS7Et72QJAHepT +sJz/GUvcgEPXKvR47w3gULh2x5LL6fiN5AQt0Rdmo7pclCdo/bq7bZ+YgdLygbjz +qNx8ulT5F7uYQJ+vlwJBANb7kNO0bFFErtIFRuGpwJvzglUeWNz+Y2JQ/IeGm+wy +lxIvpYYbJArqZjUochqaqoNBkBXYAOToTuYSLH/rAYE= +-----END RSA PRIVATE KEY----- diff --git a/imaginary.go b/imaginary.go index e5080237..ed0a71e0 100644 --- a/imaginary.go +++ b/imaginary.go @@ -3,35 +3,36 @@ package main import ( "flag" "fmt" + . "github.com/tj/go-debug" "os" "runtime" d "runtime/debug" "strconv" "time" - - . "github.com/tj/go-debug" ) var debug = Debug("imaginary") var ( - aAddr = flag.String("a", "", "bind address") - aPort = flag.Int("p", 8088, "port to listen") - aVers = flag.Bool("v", false, "") - aVersl = flag.Bool("version", false, "") - aHelp = flag.Bool("h", false, "") - aHelpl = flag.Bool("help", false, "") - aCors = flag.Bool("cors", false, "") - aGzip = flag.Bool("gzip", false, "") - aKey = flag.String("key", "", "") - aMount = flag.String("mount", "", "") - aCertFile = flag.String("certfile", "", "") - aKeyFile = flag.String("keyfile", "", "") - aHttpCacheTtl = flag.Int("http-cache-ttl", -1, "The TTL in seconds") - aConcurrency = flag.Int("concurrency", 0, "") - aBurst = flag.Int("burst", 100, "") - aMRelease = flag.Int("mrelease", 30, "") - aCpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "") + aAddr = flag.String("a", "", "bind address") + aPort = flag.Int("p", 8088, "port to listen") + aVers = flag.Bool("v", false, "") + aVersl = flag.Bool("version", false, "") + aHelp = flag.Bool("h", false, "") + aHelpl = flag.Bool("help", false, "") + aCors = flag.Bool("cors", false, "") + aGzip = flag.Bool("gzip", false, "") + aKey = flag.String("key", "", "") + aMount = flag.String("mount", "", "") + aCertFile = flag.String("certfile", "", "") + aKeyFile = flag.String("keyfile", "", "") + aHttpCacheTtl = flag.Int("http-cache-ttl", -1, "The TTL in seconds") + aReadTimeout = flag.Int("http-read-timeout", 30, "HTTP read timeout in seconds") + aWriteTimeout = flag.Int("http-write-timeout", 30, "HTTP write timeout in seconds") + aConcurrency = flag.Int("concurrency", 0, "") + aBurst = flag.Int("burst", 100, "") + aMRelease = flag.Int("mrelease", 30, "") + aCpus = flag.Int("cpus", runtime.GOMAXPROCS(-1), "") ) const usage = `imaginary server %s @@ -39,26 +40,29 @@ const usage = `imaginary server %s Usage: imaginary -p 80 imaginary -cors -gzip + imaginary -concurrency 10 imaginary -h | -help imaginary -v | -version Options: - -a bind address [default: *] - -p bind port [default: 8088] - -h, -help output help - -v, -version output version - -cors Enable CORS support [default: false] - -gzip Enable gzip compression [default: false] - -key Define API key for authorization - -mount Mount server directory - -http-cache-ttl The TTL in seconds. Adds caching headers to locally served files. - -certfile TLS certificate file path - -keyfile TLS key file path - -concurreny Throttle concurrency limit per second [default: disabled] - -burst Throttle burst max cache size [default: 100] - -mrelease Force OS memory release inverval in seconds [default: 30] - -cpus Number of used cpu cores. - (default for current machine is %d cores) + -a bind address [default: *] + -p bind port [default: 8088] + -h, -help output help + -v, -version output version + -cors Enable CORS support [default: false] + -gzip Enable gzip compression [default: false] + -key Define API key for authorization + -mount Mount server directory + -http-cache-ttl The TTL in seconds. Adds caching headers to locally served files. + -http-read-timeout HTTP read timeout in seconds [default: 30] + -http-write-timeout HTTP read timeout in seconds [default: 30] + -certfile TLS certificate file path + -keyfile TLS key file path + -concurreny Throttle concurrency limit per second [default: disabled] + -burst Throttle burst max cache size [default: 100] + -mrelease Force OS memory release inverval in seconds [default: 30] + -cpus Number of used cpu cores. + (default for current machine is %d cores) ` func main() { @@ -79,17 +83,19 @@ func main() { port := getPort(*aPort) opts := ServerOptions{ - Port: port, - Address: *aAddr, - Gzip: *aGzip, - CORS: *aCors, - ApiKey: *aKey, - Concurrency: *aConcurrency, - Burst: *aBurst, - Mount: *aMount, - CertFile: *aCertFile, - KeyFile: *aKeyFile, - HttpCacheTtl: *aHttpCacheTtl, + Port: port, + Address: *aAddr, + Gzip: *aGzip, + CORS: *aCors, + ApiKey: *aKey, + Concurrency: *aConcurrency, + Burst: *aBurst, + Mount: *aMount, + CertFile: *aCertFile, + KeyFile: *aKeyFile, + HttpCacheTtl: *aHttpCacheTtl, + HttpReadTimeout: *aReadTimeout, + HttpWriteTimeout: *aWriteTimeout, } // Create a memory release goroutine diff --git a/server.go b/server.go index 74894173..8d7ff8fe 100644 --- a/server.go +++ b/server.go @@ -8,17 +8,19 @@ import ( ) type ServerOptions struct { - Port int - CORS bool - Gzip bool - Address string - ApiKey string - Mount string - CertFile string - KeyFile string - Burst int - Concurrency int - HttpCacheTtl int + Port int + CORS bool + Gzip bool + Address string + ApiKey string + Mount string + CertFile string + KeyFile string + Burst int + Concurrency int + HttpCacheTtl int + HttpReadTimeout int + HttpWriteTimeout int } func Server(o ServerOptions) error { @@ -26,11 +28,11 @@ func Server(o ServerOptions) error { handler := NewLog(NewServerMux(o), os.Stdout) server := &http.Server{ - Addr: addr, - Handler: handler, - ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, - MaxHeaderBytes: 1 << 20, + Addr: addr, + Handler: handler, + ReadTimeout: time.Duration(o.HttpReadTimeout) * time.Second, + WriteTimeout: time.Duration(o.HttpWriteTimeout) * time.Second, + MaxHeaderBytes: 1 << 20, } return listenAndServe(server, o) diff --git a/source.go b/source.go new file mode 100644 index 00000000..dd95b867 --- /dev/null +++ b/source.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "net/http" + "os" +) + +type ImageSourceType string +type ImageSourceFactoryFunction func(*SourceConfig) ImageSource + +type SourceConfig struct { + Type ImageSourceType + Directory string +} + +var ( + imageSourceTypeToFactoryFunctionMap = make(map[ImageSourceType]ImageSourceFactoryFunction) +) + +type ImageSource interface { + GetImage(*http.Request) ([]byte, error) +} + +func RegisterSource(sourceType ImageSourceType, factory ImageSourceFactoryFunction) { + imageSourceTypeToFactoryFunctionMap[sourceType] = factory +} + +func NewImageSourceWithConfig(config *SourceConfig) ImageSource { + factory := imageSourceTypeToFactoryFunctionMap[config.Type] + if factory == nil { + fmt.Fprintf(os.Stderr, "Unknown image source type: %s\n", config.Type) + os.Exit(1) + } + return factory(config) +} diff --git a/source_http.go b/source_http.go new file mode 100644 index 00000000..8ac1aee0 --- /dev/null +++ b/source_http.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" +) + +const ( + ImageSourceTypeHttp ImageSourceType = "http" +) + +type HttpImageSource struct { + Config *SourceConfig +} + +func NewHttpImageSourceWithConfig(config *SourceConfig) ImageSource { + return &HttpImageSource{config} +} + +func GetImageFromReader(buffer io.Reader) ([]byte, error) { + return ioutil.ReadAll(buffer) +} + +func (s *HttpImageSource) GetImage(request *http.Request) ([]byte, error) { + URL, err := s.parseURL(request) + if err != nil { + return nil, err + } + + httpRequest := s.newHttpRequest(URL) + httpResponse, err := http.DefaultClient.Do(httpRequest) + defer httpResponse.Body.Close() + if err != nil { + return nil, fmt.Errorf("Error downloading image: %v", err) + } + if httpResponse.StatusCode != 200 { + return nil, fmt.Errorf("Error downloading image (url=%s)", httpRequest.URL.RequestURI()) + } + + body, err := GetImageFromReader(httpResponse.Body) + if err != nil { + return nil, fmt.Errorf("Unable to create image from response body: %v (url=%s)", string(body), httpRequest.URL.RequestURI()) + } + + return body, nil +} + +func (s *HttpImageSource) parseURL(request *http.Request) (*url.URL, error) { + queryUrl := request.URL.Query().Get("url") + return url.Parse(queryUrl) +} + +func (s *HttpImageSource) newHttpRequest(url *url.URL) *http.Request { + httpRequest, _ := http.NewRequest("GET", url.RequestURI(), nil) + httpRequest.URL = url + return httpRequest +} + +func init() { + RegisterSource(ImageSourceTypeHttp, NewHttpImageSourceWithConfig) +}