Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

conditions: datetime headers supports new TZs #4087

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 30 additions & 26 deletions cmd/object-handlers-common.go
Expand Up @@ -66,23 +66,27 @@ func checkCopyObjectPreconditions(w http.ResponseWriter, r *http.Request, objInf
// since the specified time otherwise return 412 (precondition failed).
ifModifiedSinceHeader := r.Header.Get("x-amz-copy-source-if-modified-since")
if ifModifiedSinceHeader != "" {
if !ifModifiedSince(objInfo.ModTime, ifModifiedSinceHeader) {
// If the object is not modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
if givenTime, err := parseTime(time.RFC1123, ifModifiedSinceHeader); err == nil {
if !ifModifiedSince(objInfo.ModTime, givenTime) {
// If the object is not modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
}
}
}

// x-amz-copy-source-if-unmodified-since : Return the object only if it has not been
// modified since the specified time, otherwise return a 412 (precondition failed).
ifUnmodifiedSinceHeader := r.Header.Get("x-amz-copy-source-if-unmodified-since")
if ifUnmodifiedSinceHeader != "" {
if ifModifiedSince(objInfo.ModTime, ifUnmodifiedSinceHeader) {
// If the object is modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
if givenTime, err := parseTime(time.RFC1123, ifUnmodifiedSinceHeader); err == nil {
if ifModifiedSince(objInfo.ModTime, givenTime) {
// If the object is modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
}
}
}

Expand Down Expand Up @@ -147,23 +151,27 @@ func checkPreconditions(w http.ResponseWriter, r *http.Request, objInfo ObjectIn
// otherwise return a 304 (not modified).
ifModifiedSinceHeader := r.Header.Get("If-Modified-Since")
if ifModifiedSinceHeader != "" {
if !ifModifiedSince(objInfo.ModTime, ifModifiedSinceHeader) {
// If the object is not modified since the specified time.
writeHeaders()
w.WriteHeader(http.StatusNotModified)
return true
if givenTime, err := parseTime(time.RFC1123, ifModifiedSinceHeader); err == nil {
if !ifModifiedSince(objInfo.ModTime, givenTime) {
// If the object is not modified since the specified time.
writeHeaders()
w.WriteHeader(http.StatusNotModified)
return true
}
}
}

// If-Unmodified-Since : Return the object only if it has not been modified since the specified
// time, otherwise return a 412 (precondition failed).
ifUnmodifiedSinceHeader := r.Header.Get("If-Unmodified-Since")
if ifUnmodifiedSinceHeader != "" {
if ifModifiedSince(objInfo.ModTime, ifUnmodifiedSinceHeader) {
// If the object is modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
if givenTime, err := parseTime(time.RFC1123, ifUnmodifiedSinceHeader); err == nil {
if ifModifiedSince(objInfo.ModTime, givenTime) {
// If the object is modified since the specified time.
writeHeaders()
writeErrorResponse(w, ErrPreconditionFailed, r.URL)
return true
}
}
}

Expand Down Expand Up @@ -195,14 +203,10 @@ func checkPreconditions(w http.ResponseWriter, r *http.Request, objInfo ObjectIn
}

// returns true if object was modified after givenTime.
func ifModifiedSince(objTime time.Time, givenTimeStr string) bool {
givenTime, err := time.Parse(http.TimeFormat, givenTimeStr)
if err != nil {
return true
}
func ifModifiedSince(objTime time.Time, givenTime time.Time) bool {
// The Date-Modified header truncates sub-second precision, so
// use mtime < t+1s instead of mtime <= t to check for unmodified.
if objTime.After(givenTime.Add(1 * time.Second)) {
if objTime.UTC().After(givenTime.UTC().Add(1 * time.Second)) {
return true
}
return false
Expand Down
37 changes: 35 additions & 2 deletions cmd/server_test.go
Expand Up @@ -1280,6 +1280,9 @@ func (s *TestSuiteCommon) TestHeadOnObjectLastModified(c *C) {
int64(buffer1.Len()), buffer1, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)

laTZ, err := time.LoadLocation("America/Los_Angeles")
c.Assert(err, IsNil)

// executing the HTTP request to download the object.
response, err = client.Do(request)
c.Assert(err, IsNil)
Expand All @@ -1306,7 +1309,18 @@ func (s *TestSuiteCommon) TestHeadOnObjectLastModified(c *C) {
request, err = newTestSignedRequest("HEAD", getHeadObjectURL(s.endPoint, bucketName, objectName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)
request.Header.Set("If-Modified-Since", t.Add(10*time.Minute).UTC().Format(http.TimeFormat))
request.Header.Set("If-Modified-Since", t.Add(10*time.Minute).UTC().Format(time.RFC1123))
response, err = client.Do(request)
c.Assert(err, IsNil)
// Since the "If-Modified-Since" header was ahead in time compared to the actual
// modified time of the object expecting the response status to be http.StatusNotModified.
c.Assert(response.StatusCode, Equals, http.StatusNotModified)

// Test If-Unmodified-Since with a timezone other than GMT
request, err = newTestSignedRequest("HEAD", getHeadObjectURL(s.endPoint, bucketName, objectName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)
request.Header.Set("If-Modified-Since", t.Add(10*time.Minute).In(laTZ).Format(time.RFC1123))
response, err = client.Do(request)
c.Assert(err, IsNil)
// Since the "If-Modified-Since" header was ahead in time compared to the actual
Expand All @@ -1319,10 +1333,29 @@ func (s *TestSuiteCommon) TestHeadOnObjectLastModified(c *C) {
request, err = newTestSignedRequest("HEAD", getHeadObjectURL(s.endPoint, bucketName, objectName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)
request.Header.Set("If-Unmodified-Since", t.Add(-10*time.Minute).UTC().Format(http.TimeFormat))
request.Header.Set("If-Unmodified-Since", t.Add(-10*time.Minute).UTC().Format(time.RFC1123))
response, err = client.Do(request)
c.Assert(err, IsNil)
c.Assert(response.StatusCode, Equals, http.StatusPreconditionFailed)

// Test If-Unmodified-Since with a timezone other than GMT
request, err = newTestSignedRequest("HEAD", getHeadObjectURL(s.endPoint, bucketName, objectName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)
request.Header.Set("If-Unmodified-Since", t.Add(-10*time.Minute).In(laTZ).Format(time.RFC1123))
response, err = client.Do(request)
c.Assert(err, IsNil)
c.Assert(response.StatusCode, Equals, http.StatusPreconditionFailed)

// Test If-Unmodified-Since with a timezone other than GMT
request, err = newTestSignedRequest("HEAD", getHeadObjectURL(s.endPoint, bucketName, objectName),
0, nil, s.accessKey, s.secretKey, s.signer)
c.Assert(err, IsNil)
request.Header.Set("If-Unmodified-Since", t.Add(10*time.Minute).In(laTZ).Format(time.RFC1123))
response, err = client.Do(request)
c.Assert(err, IsNil)
c.Assert(response.StatusCode, Equals, http.StatusOK)

}

// TestHeadOnBucket - Validates response for HEAD on the bucket.
Expand Down
67 changes: 67 additions & 0 deletions cmd/time.go
@@ -0,0 +1,67 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cmd

import "time"

var minioTZ = map[string]int{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to expand this.. but i think we are better off finding a better approach here. This will make it harder to add new timezones etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we are better off finding a better approach here. This will make it harder to add new timezones etc.

I tried, but there is no other way.

We need to expand this..

We can add a new dependency for a more complete list.


"PST": -8 * 3600,
"PDT": -7 * 3600,

"EST": -5 * 3600,
"EDT": -4 * 3600,

"GMT": 0,
"UTC": 0,

"CET": 1 * 3600,
"CEST": 2 * 3600,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only these timezone? what is 3600? why can't we use time.Hour constant?

}

// parseTime returns the correct time when a timezone is provided
// with the time string, if the specified timezone is not found in
// minioTZ, call only the standard golang time parsing.
func parseTime(layout, value string) (time.Time, error) {
// Standard golang time parsing
t, err := time.Parse(layout, value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@vadmeste vadmeste Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can we use https://golang.org/pkg/time/#ParseInLocation?

We can't, ParseInLocation needs location as parameter and the purpose of this PR is to guess the location from the date string.

For example, if we have this date Fri, 03 Apr 2015 17:10:00 PST, golang can parse this string but it doesn't consider PST because it doesn't know it. The parsed date will automatically have UTC timezone which is wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My observation on time/timezone in Go is below

  • Go has an issue supporting abbreviated timezone.
  • time.Parse() sets abbreviated timezone text, not correct value to know time difference with UTC.
  • However time.ParseInLocation() works properly when timezone is passed as argument.
  • As RFC1123 carries abbreviated timezone we have problem with name collision eg. CST refers to multiple timezone.

Based on the above we could do

  • No support other than GMT as per RFC.
  • If we support other than GMT, we would need to support all of them (Note: there is no solution to abbreviated timezone name collision).

If we go with option 2, the right fix would just have a map of abbreviated name to full name and use time.ParseInLocation() like below

var NameLocationMap = map[string]string{"PST":"US/Pacific", ...}
t, err = time.Parse(time.RFC1123, timeString)
l, err = t.Location()
location, ok = NameLocationMap[l.String()]
if ok {
    l, err = time.LoadLocation(location)
    t, err = time.ParseInLocation(time.RFC1123, timeString, l)
}

fmt.Println("local time:", t.Local())
fmt.Println("UTC time:", t.UTC())

Copy link
Member Author

@vadmeste vadmeste Apr 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@balamurugana, the problem with locations is that they are taking care of summer time shift, here US/Pacific can be PST or PDT, that's why I opted the time offset solution (-8, -7, ..)

I knew about timezone collision, but AWS S3 is already doing that..

if err != nil {
return time.Time{}, err
}

// Fetch the location of the passed time
loc := t.Location()

if loc == nil || loc == time.UTC || loc == time.Local {
// Nothing to do, even when location is set to time.Local
// since time will be obviously correct with the local
// machine's timezone
return t, nil
}

// Fetch the time offset associated to the passed timezone.
// If not found, return golang std time parser result.
offset, ok := minioTZ[loc.String()]
if !ok {
return t, nil
}

// Calculate the new time
newTime := t.Add(-time.Duration(offset) * time.Second)

return newTime, nil
}
52 changes: 52 additions & 0 deletions cmd/time_test.go
@@ -0,0 +1,52 @@
/*
* Minio Cloud Storage, (C) 2017 Minio, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cmd

import (
"testing"
"time"
)

func TestParseTime(t *testing.T) {
testCases := []struct {
timeStr string
timeLayout string
timeUTC time.Time
shouldPass bool
}{
{"", time.RFC1123, time.Time{}, false},
{"foo", time.RFC1123, time.Time{}, false},
{"Fri, 03 Apr 2015 17:10:00 GMT", time.RFC1123, time.Date(2015, 04, 03, 17, 10, 00, 00, time.UTC), true},
{"Fri, 03 Apr 2015 17:10:00 UTC", time.RFC1123, time.Date(2015, 04, 03, 17, 10, 00, 00, time.UTC), true},
{"Fri, 03 Apr 2015 18:10:00 CET", time.RFC1123, time.Date(2015, 04, 03, 17, 10, 00, 00, time.UTC), true},
{"Fri, 03 Apr 2015 19:10:00 CEST", time.RFC1123, time.Date(2015, 04, 03, 17, 10, 00, 00, time.UTC), true},
{"Fri, 03 Apr 2015 09:10:00 PST", time.RFC1123, time.Date(2015, 04, 03, 17, 10, 00, 00, time.UTC), true},
}

for i, testCase := range testCases {
parsedTime, err := parseTime(testCase.timeLayout, testCase.timeStr)
if err == nil && !testCase.shouldPass {
t.Errorf("Test %d expected to fail but passed instead\n", i+1)
}
if err != nil && testCase.shouldPass {
t.Errorf("Test %d expected to pass but failed with err: %v\n", i+1, err)
}
if !parsedTime.UTC().Equal(testCase.timeUTC) {
t.Errorf("Test %d found an unexpected result, found: %v, expected: %v\n", i+1, parsedTime.UTC(), testCase.timeUTC)
}
}
}