@@ -58,15 +58,21 @@ func ValidateProfileName(name string) error {
5858 return nil
5959}
6060
61- // ReadConfig returns host and token for profile.
62- // OP_CLI_HOST and OP_CLI_TOKEN always take precedence over the file .
61+ // ReadConfig returns the host and token for profile. Environment credentials
62+ // take precedence and do not require the config directory to be accessible .
6363func ReadConfig (profile string ) (host , token string , err error ) {
64- if err = ensureConfigDir (); err != nil {
65- return "" , "" , err
66- }
6764 if ok , h , t := readEnvironment (); ok {
65+ if err := validateHost (h ); err != nil {
66+ return "" , "" , errors .Custom (fmt .Sprintf (
67+ "environment variable OP_CLI_HOST has an invalid host %q: %v" ,
68+ h , err ,
69+ ))
70+ }
6871 return h , t , nil
6972 }
73+ if err = ensureConfigDir (); err != nil {
74+ return "" , "" , err
75+ }
7076 return readConfigForProfile (profile )
7177}
7278
@@ -245,12 +251,26 @@ func (f *iniFile) marshal() []byte {
245251 return []byte (sb .String ())
246252}
247253
248- // looksLikeHost reports whether s parses as an absolute URL with a scheme and
249- // host. Used to distinguish a genuine old-format "host token" line from a
250- // corrupt file, so garbage is not silently migrated into bogus credentials.
254+ // looksLikeHost reports whether s is an absolute HTTP(S) URL. It distinguishes
255+ // the legacy "host token" format from corrupt configuration content.
251256func looksLikeHost (s string ) bool {
257+ return validateHost (s ) == nil
258+ }
259+
260+ // validateHost rejects relative URLs and schemes the HTTP client cannot use.
261+ func validateHost (s string ) error {
252262 u , err := url .Parse (s )
253- return err == nil && u .Scheme != "" && u .Host != ""
263+ if err != nil {
264+ return fmt .Errorf ("invalid host URL: %w" , err )
265+ }
266+ if u .Scheme == "" || u .Host == "" {
267+ return fmt .Errorf ("expected an absolute URL like https://example.openproject.com" )
268+ }
269+ scheme := strings .ToLower (u .Scheme )
270+ if scheme != "http" && scheme != "https" {
271+ return fmt .Errorf ("unsupported protocol scheme %q: only http and https are allowed" , u .Scheme )
272+ }
273+ return nil
254274}
255275
256276// readOrMigrate reads the config file and migrates it from the old
@@ -360,14 +380,13 @@ func readConfigForProfile(profile string) (host, token string, err error) {
360380 if f .hasSection (profile ) && (host == "" || token == "" ) {
361381 return "" , "" , invalidConfigError ()
362382 }
363- // Reject junk hosts here with a clear message; url.Parse alone accepts
364- // almost anything as a relative URL, which would surface later as a
365- // confusing request failure.
366- if host != "" && ! looksLikeHost (host ) {
367- return "" , "" , errors .Custom (fmt .Sprintf (
368- "profile %q has an invalid host %q: expected an absolute URL like https://example.openproject.com" ,
369- profile , host ,
370- ))
383+ if host != "" {
384+ if err := validateHost (host ); err != nil {
385+ return "" , "" , errors .Custom (fmt .Sprintf (
386+ "profile %q has an invalid host %q: %v" ,
387+ profile , host , err ,
388+ ))
389+ }
371390 }
372391 return host , token , nil
373392}
0 commit comments