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

feat: enable --header flag to specify headers with requests #794

Merged
merged 8 commits into from
Feb 14, 2023
Merged

feat: enable --header flag to specify headers with requests #794

merged 8 commits into from
Feb 14, 2023

Conversation

wangxiaoxuan273
Copy link
Contributor

@wangxiaoxuan273 wangxiaoxuan273 commented Feb 9, 2023

Resolves #521

Designed in accordance to the behavior(as exact spec can't be found) of curl-H flag. Can be used multiple times.

Terminal output:
Screenshot 2023-02-10 at 00 22 43

Some minor issues/questions:

  1. when parsing input like "c: d, e, f" into map[string][]string of golang, the spaces before and after the commas are not stripped. i.e. the values are "d", " e", " f".
    (update: splitting is removed, the argument is kept as what is after the colon.)
  2. curl parses input -H "a:b" -H "a:b" to two repeated headers "a:b", and -H "a:b" -H "a:c" to headers "a:b", "a:c". In this pr these behaviors are maintained.

@wangxiaoxuan273 wangxiaoxuan273 changed the title feat: enable custom headers with requests feat: enable --header flag to specify headers with requests Feb 9, 2023
Copy link
Member

@TerryHowe TerryHowe left a comment

Choose a reason for hiding this comment

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

generally looks good to me, couple comments

cmd/oras/internal/option/remote.go Outdated Show resolved Hide resolved
before, after, found := strings.Cut(h, ":")
if found && after != "" {
if headers[before] == nil {
headers[before] = strings.Split(after, ",")
Copy link
Member

Choose a reason for hiding this comment

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

I hadn't noticed this syntax before in other commands. It looks convenient, but not sure people will think to use it.

Copy link
Contributor Author

@wangxiaoxuan273 wangxiaoxuan273 Feb 10, 2023

Choose a reason for hiding this comment

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

Did you mean the argument of the -H command? I designed it according to the behavior of curl -H flag. @TerryHowe

Copy link
Member

Choose a reason for hiding this comment

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

No, I was talking about the way you can provide multiple header values with the comma separation.

Copy link
Contributor

Choose a reason for hiding this comment

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

@TerryHowe Are you suggesting we shouldn't unbox the input value and just forward what is provided?

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong opinion about it, but it seemed a bit odd. I don't think curl uses that comma separated format. I think you just have to specify the same header with multiple -H flags

Copy link
Contributor

@qweeah qweeah Feb 10, 2023

Choose a reason for hiding this comment

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

curl don't use comma to separate header values because:

  • value parsing can be done on server side. This also implies that oras don't need to split any header value.
  • the header key can be duplicated for curl. This is different here, because in golang, the underlying data struct http.Header is a map, and thus header key cannot be duplicated. So we might need to concatenate duplicated header with comma explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: removed comma unboxing.

Copy link
Contributor

@qweeah qweeah left a comment

Choose a reason for hiding this comment

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

Made some comments on implementation and UX. Thanks again for contributing!

cmd/oras/internal/option/remote.go Outdated Show resolved Hide resolved
if len(opts.headers) != 0 {
headers := map[string][]string{}
for _, h := range opts.headers {
before, after, found := strings.Cut(h, ":")
Copy link
Contributor

Choose a reason for hiding this comment

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

UX: should return an error if a flag is not formatted as <name>:<comma-separated-values>, so users can be aware of the potential errors in their input.
cc @FeynmanZhou

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Returned error currently, need further discussion and confirmation.

cmd/oras/internal/option/remote.go Outdated Show resolved Hide resolved
@qweeah
Copy link
Contributor

qweeah commented Feb 10, 2023

when parsing input like "c: d, e, f" into map[string][]string of golang, the spaces before and after the commas are not stripped. i.e. the values are "d", " e", " f".

We can just forward the value with trailing space. By design it should be ignored. I didn't find the RFC for this but there are some MDN docs mentioning it.

curl parses input -H "a:b" -H "a:b" to two repeated headers "a:b", and -H "a:b" -H "a:c" to headers "a:b", "a:c". In this pr these behaviors are maintained.

Shouldn't -H "a:b" -H "a:c" be parsed to a:b,c(in your implementation)?

@codecov-commenter
Copy link

codecov-commenter commented Feb 10, 2023

Codecov Report

Merging #794 (a254d7a) into main (d6240d6) will increase coverage by 0.52%.
The diff coverage is 84.21%.

📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more

@@            Coverage Diff             @@
##             main     #794      +/-   ##
==========================================
+ Coverage   55.50%   56.03%   +0.52%     
==========================================
  Files          23       23              
  Lines         935      953      +18     
==========================================
+ Hits          519      534      +15     
- Misses        374      376       +2     
- Partials       42       43       +1     
Impacted Files Coverage Δ
cmd/oras/internal/option/remote.go 69.34% <84.21%> (+1.39%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

Comment on lines 258 to 262
name, csv, found := strings.Cut(h, ":")
if !found || strings.TrimSpace(csv) == "" {
return fmt.Errorf("cannot parse headers: %q", h)
}
headers[name] = append(headers[name], strings.Split(csv, ",")...)
Copy link
Contributor

@qweeah qweeah Feb 10, 2023

Choose a reason for hiding this comment

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

No need to unbox the value, just add it to the http headers.

Suggested change
name, csv, found := strings.Cut(h, ":")
if !found || strings.TrimSpace(csv) == "" {
return fmt.Errorf("cannot parse headers: %q", h)
}
headers[name] = append(headers[name], strings.Split(csv, ",")...)
name, value, found := strings.Cut(h, ":")
if !found || strings.TrimSpace(name) == "" || strings.TrimSpace(value) == "" {
return fmt.Errorf("invalid header: %q", h)
}
headers[name] = append(headers[name], value)

Copy link
Contributor Author

@wangxiaoxuan273 wangxiaoxuan273 Feb 13, 2023

Choose a reason for hiding this comment

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

removed the unboxing. Behavior changed according to the RFC 2616 spec.

@@ -51,6 +51,9 @@ type Remote struct {
resolveDialContext func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error)
applyDistributionSpec bool
distributionSpec distributionSpec

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove empty line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed.

if !found || strings.TrimSpace(csv) == "" {
return fmt.Errorf("cannot parse headers: %q", h)
}
headers[name] = append(headers[name], strings.Split(csv, ",")...)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should consume the value as it is without splitting.

You can try -H "foo: a" -H "foo: b,c" with curl. It will generate

> foo: a
> foo: b,c

Copy link
Contributor Author

@wangxiaoxuan273 wangxiaoxuan273 Feb 10, 2023

Choose a reason for hiding this comment

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

Changed according to Billy's comment above. Splitting is removed.

for _, h := range opts.headerFlags {
name, value, found := strings.Cut(h, ":")
if !found || strings.TrimSpace(value) == "" {
return fmt.Errorf("cannot parse headers: %q", h)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
return fmt.Errorf("cannot parse headers: %q", h)
return fmt.Errorf("invalid header: %q", h)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed accordingly.

headers := map[string][]string{}
for _, h := range opts.headerFlags {
name, value, found := strings.Cut(h, ":")
if !found || strings.TrimSpace(value) == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

According to RFC 2616 Section 4.2, the name must not be empty and the value can be empty.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note:

package main

import (
	"net/http"
	"os"
)

func main() {
	headers := make(http.Header)
	headers[""] = []string{"abc"}
	headers["    "] = []string{"efg"}
	headers["empty"] = []string{""}
	headers["space"] = []string{"   "}
	headers["foo"] = []string{"bar"}
	headers["foo2"] = []string{"  bar2"}

	headers.Write(os.Stdout)
}

outputs

empty: 
foo: bar
foo2: bar2
space: 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to RFC 2616 Section 4.2, the name must not be empty and the value can be empty.

curl's behavior is the other way around. Which should I follow, curl or the RFC spec?

Copy link
Contributor

Choose a reason for hiding this comment

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

Always follow the spec since the spec is the standard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the behavior of parsing to be conformant to the spec.

Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
@wangxiaoxuan273 wangxiaoxuan273 requested review from shizhMSFT and qweeah and removed request for shizhMSFT February 13, 2023 07:14
cmd/oras/internal/option/remote_test.go Outdated Show resolved Hide resolved
cmd/oras/internal/option/remote_test.go Outdated Show resolved Hide resolved
Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Copy link
Contributor

@qweeah qweeah left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@shizhMSFT shizhMSFT left a comment

Choose a reason for hiding this comment

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

LGTM with suggestions

@@ -62,6 +64,7 @@ func (opts *Remote) EnableDistributionSpecFlag() {
func (opts *Remote) ApplyFlags(fs *pflag.FlagSet) {
opts.ApplyFlagsWithPrefix(fs, "", "")
fs.BoolVarP(&opts.PasswordFromStdin, "password-stdin", "", false, "read password or identity token from stdin")
fs.StringArrayVarP(&opts.headerFlags, "header", "H", []string{}, "add custom headers to requests")
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: in golang, nil slice is still a valid slice.

Suggested change
fs.StringArrayVarP(&opts.headerFlags, "header", "H", []string{}, "add custom headers to requests")
fs.StringArrayVarP(&opts.headerFlags, "header", "H", nil, "add custom headers to requests")

Signed-off-by: wangxiaoxuan273 <wangxiaoxuan119@gmail.com>
Copy link
Contributor

@shizhMSFT shizhMSFT left a comment

Choose a reason for hiding this comment

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

LGTM

@shizhMSFT shizhMSFT merged commit 1543ce2 into oras-project:main Feb 14, 2023
@wangxiaoxuan273 wangxiaoxuan273 deleted the headers branch February 14, 2023 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support specifying http headers via --header flag
5 participants