Skip to content

Commit

Permalink
make HTTP Cache-Control & Expires headers configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
pottava committed Feb 15, 2016
1 parent af4b546 commit 0b7aa80
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 35 deletions.
6 changes: 6 additions & 0 deletions README-ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
API 経由でアクセスするため、バケットに静的 Web サイトホスティングの設定は不要です。
オプションでフロントに Basic 認証がかけられます。

http://this-proxy.com/access/ -> s3://backet/access/index.html


## 使い方

Expand All @@ -15,9 +17,13 @@ API 経由でアクセスするため、バケットに静的 Web サイトホ
環境変数 | 説明 | 必須 | 初期値
------------------------- | ----------------------------------------------- | ------ | ---
AWS_S3_BUCKET | プロキシ先の S3 バケット | * |
AWS_S3_KEY_PREFIX | S3 オブジェクトにプリフィクス文字列があるなら指定 | | -
AWS_REGION | バケットの存在する AWS リージョン | | us-east-1
AWS_ACCESS_KEY_ID | API を使うための AWS アクセスキー | | EC2 インスタンスロール
AWS_SECRET_ACCESS_KEY | API を使うための AWS シークレットキー | | EC2 インスタンスロール
HTTP_CACHE_CONTROL | S3 の `Cache-Control` 属性を上書きして返します | | S3 オブジェクト属性値
HTTP_EXPIRES | S3 の `Expires` 属性を上書きして返します | | S3 オブジェクト属性値
BASIC_AUTH_USER | Basic 認証をかけるなら、その `ユーザ名` | | -
BASIC_AUTH_PASS | Basic 認証をかけるなら、その `パスワード` | | -
SSL_CERT_PATH | TLS を有効にしたいなら、その `cert.pem` へのパス | | -
SSL_KEY_PATH | TLS を有効にしたいなら、その `key.pem` へのパス | | -
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

This is a reverse proxy for AWS S3, which is able to provide basic authentication as well.
You don't need to configure a Bucket for `Website Hosting`.

http://this-proxy.com/access/ -> s3://backet/access/index.html

([日本語はこちら](https://github.com/pottava/aws-s3-proxy/blob/master/README-ja.md))


Expand All @@ -16,9 +19,12 @@ You don't need to configure a Bucket for `Website Hosting`.
Environment Variables | Description | Required | Default
------------------------- | ------------------------------------------------- | -------- | -----------------
AWS_S3_BUCKET | The `S3 bucket` to be proxied with this app. | * |
AWS_S3_KEY_PREFIX | You can configure `S3 object key` prefix. | | -
AWS_REGION | The AWS `region` where the S3 bucket exists. | | us-east-1
AWS_ACCESS_KEY_ID | AWS `access key` for API access. | | EC2 Instance Role
AWS_SECRET_ACCESS_KEY | AWS `secret key` for API access. | | EC2 Instance Role
HTTP_CACHE_CONTROL | Overrides S3's HTTP `Cache-Control` header. | | S3 Object metadata
HTTP_EXPIRES | Overrides S3's HTTP `Expires` header. | | S3 Object metadata
BASIC_AUTH_USER | User for basic authentication. | | -
BASIC_AUTH_PASS | Password for basic authentication. | | -
SSL_CERT_PATH | TLS: cert.pem file path. | | -
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ services:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_S3_BUCKET
- AWS_S3_KEY_PREFIX
- HTTP_CACHE_CONTROL
- HTTP_EXPIRES
- BASIC_AUTH_USER
- BASIC_AUTH_PASS
- SSL_CERT_PATH
Expand Down
112 changes: 77 additions & 35 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ import (
)

type config struct {
awsRegion string // AWS_REGION
s3Bucket string // AWS_S3_BUCKET
basicAuthUser string // BASIC_AUTH_USER
basicAuthPass string // BASIC_AUTH_PASS
port string // APP_PORT
accessLog bool // ACCESS_LOG
sslCert string // SSL_CERT_PATH
sslKey string // SSL_KEY_PATH
awsRegion string // AWS_REGION
s3Bucket string // AWS_S3_BUCKET
s3KeyPrefix string // AWS_S3_KEY_PREFIX
httpCacheControl string // HTTP_CACHE_CONTROL (max-age=86400, no-cache ...)
httpExpires string // HTTP_EXPIRES (Thu, 01 Dec 1994 16:00:00 GMT ...)
basicAuthUser string // BASIC_AUTH_USER
basicAuthPass string // BASIC_AUTH_PASS
port string // APP_PORT
accessLog bool // ACCESS_LOG
sslCert string // SSL_CERT_PATH
sslKey string // SSL_KEY_PATH
}

var (
Expand Down Expand Up @@ -77,14 +80,17 @@ func configFromEnvironmentVariables() *config {
accessLog = b
}
conf := &config{
awsRegion: region,
s3Bucket: os.Getenv("AWS_S3_BUCKET"),
basicAuthUser: os.Getenv("BASIC_AUTH_USER"),
basicAuthPass: os.Getenv("BASIC_AUTH_PASS"),
port: port,
accessLog: accessLog,
sslCert: os.Getenv("SSL_CERT_PATH"),
sslKey: os.Getenv("SSL_KEY_PATH"),
awsRegion: region,
s3Bucket: os.Getenv("AWS_S3_BUCKET"),
s3KeyPrefix: os.Getenv("AWS_S3_KEY_PREFIX"),
httpCacheControl: os.Getenv("HTTP_CACHE_CONTROL"),
httpExpires: os.Getenv("HTTP_EXPIRES"),
basicAuthUser: os.Getenv("BASIC_AUTH_USER"),
basicAuthPass: os.Getenv("BASIC_AUTH_PASS"),
port: port,
accessLog: accessLog,
sslCert: os.Getenv("SSL_CERT_PATH"),
sslKey: os.Getenv("SSL_KEY_PATH"),
}
// Proxy
log.Printf("[config] Proxy to %v", conf.s3Bucket)
Expand All @@ -101,75 +107,111 @@ func configFromEnvironmentVariables() *config {
return conf
}

type custom struct {
http.ResponseWriter
status int
}

func (r *custom) WriteHeader(status int) {
r.ResponseWriter.WriteHeader(status)
r.status = status
}

func wrapper(f func(w http.ResponseWriter, r *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if (len(c.basicAuthUser) > 0) && (len(c.basicAuthPass) > 0) && !auth(r, c.basicAuthUser, c.basicAuthPass) {
if (len(c.basicAuthUser) > 0) && (len(c.basicAuthPass) > 0) && !auth(r) {
w.Header().Set("WWW-Authenticate", `Basic realm="REALM"`)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
f(w, r)
proc := time.Now()
addr := r.RemoteAddr
if ip, found := header(r, "X-Forwarded-For"); found {
addr = ip
}
writer := &custom{ResponseWriter: w, status: http.StatusOK}
f(writer, r)

if c.accessLog {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL)
log.Printf("[%s] %.3f %d %s %s",
addr, time.Now().Sub(proc).Seconds(),
writer.status, r.Method, r.URL)
}
})
}

func auth(r *http.Request, user, pass string) bool {
func auth(r *http.Request) bool {
if username, password, ok := r.BasicAuth(); ok {
return username == user && password == pass
return username == c.basicAuthUser &&
password == c.basicAuthPass
}
return false
}

func header(r *http.Request, key string) (string, bool) {
if r.Header == nil {
return "", false
}
if candidate := r.Header[key]; len(candidate) > 0 {
return candidate[0], true
}
return "", false
}

func awss3(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if strings.HasSuffix(path, "/") {
path += "index.html"
}
obj, err := s3get(c.s3Bucket, path)
obj, err := s3get(c.s3Bucket, c.s3KeyPrefix+path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
setStrHeader(w, "Cache-Control", obj.CacheControl)
if len(c.httpCacheControl) > 0 {
setStrHeader(w, "Cache-Control", &c.httpCacheControl)
} else {
setStrHeader(w, "Cache-Control", obj.CacheControl)
}
if len(c.httpExpires) > 0 {
setStrHeader(w, "Expires", &c.httpExpires)
} else {
setStrHeader(w, "Expires", obj.Expires)
}
setStrHeader(w, "Content-Disposition", obj.ContentDisposition)
setStrHeader(w, "Content-Encoding", obj.ContentEncoding)
setStrHeader(w, "Content-Language", obj.ContentLanguage)
setIntHeader(w, "Content-Length", obj.ContentLength)
setStrHeader(w, "Content-Range", obj.ContentRange)
setStrHeader(w, "Content-Type", obj.ContentType)
setStrHeader(w, "ETag", obj.ETag)
setStrHeader(w, "Expires", obj.Expires)
setTimeHeader(w, "Last-Modified", obj.LastModified)

io.Copy(w, obj.Body)
}

func s3get(backet, key string) (*s3.GetObjectOutput, error) {
sess := session.New(aws.NewConfig().WithRegion(c.awsRegion))
req := &s3.GetObjectInput{
Bucket: aws.String(backet),
Key: aws.String(key),
}
return s3.New(session.New(aws.NewConfig().WithRegion(c.awsRegion))).GetObject(req)
return s3.New(sess).GetObject(req)
}

func setStrHeader(w http.ResponseWriter, key string, value *string) {
if value == nil || len(*value) == 0 {
return
if value != nil && len(*value) > 0 {
w.Header().Add(key, *value)
}
w.Header().Add(key, *value)
}

func setIntHeader(w http.ResponseWriter, key string, value *int64) {
if value == nil || *value == 0 {
return
if value != nil && *value > 0 {
w.Header().Add(key, strconv.FormatInt(*value, 10))
}
w.Header().Add(key, strconv.FormatInt(*value, 10))
}

func setTimeHeader(w http.ResponseWriter, key string, value *time.Time) {
if value == nil || reflect.DeepEqual(*value, time.Time{}) {
return
if value != nil && !reflect.DeepEqual(*value, time.Time{}) {
w.Header().Add(key, value.UTC().Format(http.TimeFormat))
}
w.Header().Add(key, value.UTC().Format(http.TimeFormat))
}

0 comments on commit 0b7aa80

Please sign in to comment.