config.go 7.6 KB
Newer Older
J
Jingwen Owen Ou 已提交
1 2 3
package github

import (
J
Jingwen Owen Ou 已提交
4
	"bufio"
J
Jingwen Owen Ou 已提交
5
	"fmt"
6
	"io/ioutil"
J
Jingwen Owen Ou 已提交
7
	"os"
8
	"os/signal"
J
Jingwen Owen Ou 已提交
9
	"path/filepath"
10
	"strconv"
11
	"strings"
12
	"syscall"
13

14
	"github.com/github/hub/ui"
J
Jingwen Owen Ou 已提交
15
	"github.com/github/hub/utils"
16
	"github.com/mitchellh/go-homedir"
17
	"golang.org/x/crypto/ssh/terminal"
J
Jingwen Owen Ou 已提交
18 19
)

J
Jingwen Owen Ou 已提交
20 21 22 23
type yamlHost struct {
	User       string `yaml:"user"`
	OAuthToken string `yaml:"oauth_token"`
	Protocol   string `yaml:"protocol"`
24
	UnixSocket string `yaml:"unix_socket,omitempty"`
J
Jingwen Owen Ou 已提交
25 26
}

27
type yamlConfig map[string][]yamlHost
J
Jingwen Owen Ou 已提交
28

J
Jingwen Owen Ou 已提交
29
type Host struct {
30 31 32
	Host        string `toml:"host"`
	User        string `toml:"user"`
	AccessToken string `toml:"access_token"`
33
	Protocol    string `toml:"protocol"`
A
Alexander Pakulov 已提交
34
	UnixSocket  string `toml:"unix_socket,omitempty"`
J
Jingwen Owen Ou 已提交
35 36
}

37
type Config struct {
38
	Hosts []*Host `toml:"hosts"`
J
Jingwen Owen Ou 已提交
39 40
}

41
func (c *Config) PromptForHost(host string) (h *Host, err error) {
42 43 44
	token := c.DetectToken()
	tokenFromEnv := token != ""

45
	h = c.Find(host)
46
	if h != nil {
47
		if h.User == "" {
G
Goel 已提交
48
			utils.Check(CheckWriteable(configsFile()))
49 50 51 52 53 54 55 56 57 58 59
			// User is missing from the config: this is a broken config probably
			// because it was created with an old (broken) version of hub. Let's fix
			// it now. See issue #1007 for details.
			user := c.PromptForUser(host)
			if user == "" {
				utils.Check(fmt.Errorf("missing user"))
			}
			h.User = user
			err := newConfigService().Save(configsFile(), c)
			utils.Check(err)
		}
60 61 62 63 64 65 66 67 68 69 70 71
		if tokenFromEnv {
			h.AccessToken = token
		} else {
			return
		}
	} else {
		h = &Host{
			Host:        host,
			AccessToken: token,
			Protocol:    "https",
		}
		c.Hosts = append(c.Hosts, h)
72 73
	}

74
	client := NewClientWithHost(h)
75

76
	if !tokenFromEnv {
G
Goel 已提交
77
		utils.Check(CheckWriteable(configsFile()))
78
		err = c.authorizeClient(client, host)
79 80 81
		if err != nil {
			return
		}
82
	}
J
Jingwen Owen Ou 已提交
83

84 85 86
	currentUser, err := client.CurrentUser()
	if err != nil {
		return
J
Jingwen Owen Ou 已提交
87
	}
88
	h.User = currentUser.Login
J
Jingwen Owen Ou 已提交
89

90 91
	if !tokenFromEnv {
		err = newConfigService().Save(configsFile(), c)
92
	}
93

94 95 96 97 98 99 100 101 102 103 104 105 106 107
	return
}

func (c *Config) authorizeClient(client *Client, host string) (err error) {
	user := c.PromptForUser(host)
	pass := c.PromptForPassword(host, user)

	var code, token string
	for {
		token, err = client.FindOrCreateToken(user, pass, code)
		if err == nil {
			break
		}

108
		if ae, ok := err.(*errorInfo); ok && strings.HasPrefix(ae.Response.Header.Get("X-GitHub-OTP"), "required;") {
109 110 111 112 113 114 115 116 117 118 119
			if code != "" {
				ui.Errorln("warning: invalid two-factor code")
			}
			code = c.PromptForOTP()
		} else {
			break
		}
	}

	if err == nil {
		client.Host.AccessToken = token
120
	}
121 122

	return
J
Jingwen Owen Ou 已提交
123 124
}

125 126 127 128
func (c *Config) DetectToken() string {
	return os.Getenv("GITHUB_TOKEN")
}

129
func (c *Config) PromptForUser(host string) (user string) {
130 131 132 133 134
	user = os.Getenv("GITHUB_USER")
	if user != "" {
		return
	}

135
	ui.Printf("%s username: ", host)
136
	user = c.scanLine()
J
Jingwen Owen Ou 已提交
137

138
	return
J
Jingwen Owen Ou 已提交
139 140
}

141
func (c *Config) PromptForPassword(host, user string) (pass string) {
142 143 144 145 146
	pass = os.Getenv("GITHUB_PASSWORD")
	if pass != "" {
		return
	}

147
	ui.Printf("%s password for %s (never stored): ", host, user)
148 149 150 151
	if ui.IsTerminal(os.Stdin) {
		if password, err := getPassword(); err == nil {
			pass = password
		}
152
	} else {
153
		pass = c.scanLine()
154 155 156
	}

	return
J
Jingwen Owen Ou 已提交
157 158
}

159
func (c *Config) PromptForOTP() string {
J
Jingwen Owen Ou 已提交
160
	fmt.Print("two-factor authentication code: ")
161 162 163
	return c.scanLine()
}

164
func (c *Config) scanLine() string {
165
	var line string
J
Jingwen Owen Ou 已提交
166 167 168 169 170
	scanner := bufio.NewScanner(os.Stdin)
	if scanner.Scan() {
		line = scanner.Text()
	}
	utils.Check(scanner.Err())
J
Jingwen Owen Ou 已提交
171

172
	return line
J
Jingwen Owen Ou 已提交
173 174
}

175
func getPassword() (string, error) {
M
Mislav Marohnić 已提交
176 177
	stdin := int(syscall.Stdin)
	initialTermState, err := terminal.GetState(stdin)
178 179 180 181 182 183 184 185
	if err != nil {
		return "", err
	}

	c := make(chan os.Signal)
	signal.Notify(c, os.Interrupt, os.Kill)
	go func() {
		s := <-c
M
Mislav Marohnić 已提交
186
		terminal.Restore(stdin, initialTermState)
187 188 189 190 191 192 193 194 195
		switch sig := s.(type) {
		case syscall.Signal:
			if int(sig) == 2 {
				fmt.Println("^C")
			}
		}
		os.Exit(1)
	}()

M
Mislav Marohnić 已提交
196
	passBytes, err := terminal.ReadPassword(stdin)
197 198 199 200 201 202 203 204 205
	if err != nil {
		return "", err
	}

	signal.Stop(c)
	fmt.Print("\n")
	return string(passBytes), nil
}

206
func (c *Config) Find(host string) *Host {
J
Jingwen Owen Ou 已提交
207 208
	for _, h := range c.Hosts {
		if h.Host == host {
209
			return h
J
Jingwen Owen Ou 已提交
210 211 212 213 214 215
		}
	}

	return nil
}

216 217 218 219
func (c *Config) selectHost() *Host {
	options := len(c.Hosts)

	if options == 1 {
220
		return c.Hosts[0]
J
Jingwen Owen Ou 已提交
221 222
	}

223 224 225
	prompt := "Select host:\n"
	for idx, host := range c.Hosts {
		prompt += fmt.Sprintf(" %d. %s\n", idx+1, host.Host)
J
Jingwen Owen Ou 已提交
226
	}
227
	prompt += fmt.Sprint("> ")
J
Jingwen Owen Ou 已提交
228

229
	ui.Printf(prompt)
230 231 232 233 234
	index := c.scanLine()
	i, err := strconv.Atoi(index)
	if err != nil || i < 1 || i > options {
		utils.Check(fmt.Errorf("Error: must enter a number [1-%d]", options))
	}
J
Jingwen Owen Ou 已提交
235

236
	return c.Hosts[i-1]
J
Jingwen Owen Ou 已提交
237 238
}

239 240
var defaultConfigsFile string

J
Jingwen Owen Ou 已提交
241
func configsFile() string {
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
	if configFromEnv := os.Getenv("HUB_CONFIG"); configFromEnv != "" {
		return configFromEnv
	}
	if defaultConfigsFile == "" {
		var err error
		defaultConfigsFile, err = determineConfigLocation()
		utils.Check(err)
	}
	return defaultConfigsFile
}

func homeConfig() (string, error) {
	if home, err := homedir.Dir(); err != nil {
		return "", err
	} else {
		return filepath.Join(home, ".config"), nil
	}
}

func determineConfigLocation() (string, error) {
	var err error

	xdgHome := os.Getenv("XDG_CONFIG_HOME")
	configDir := xdgHome
	if configDir == "" {
		if configDir, err = homeConfig(); err != nil {
			return "", err
		}
	}

	xdgDirs := os.Getenv("XDG_CONFIG_DIRS")
	if xdgDirs == "" {
		xdgDirs = "/etc/xdg"
	}
	searchDirs := append([]string{configDir}, strings.Split(xdgDirs, ":")...)

	for _, dir := range searchDirs {
		filename := filepath.Join(dir, "hub")
		if _, err := os.Stat(filename); err == nil {
			return filename, nil
		}
	}

	configFile := filepath.Join(configDir, "hub")

	if configDir == xdgHome {
		if homeDir, _ := homeConfig(); homeDir != "" {
			legacyConfig := filepath.Join(homeDir, "hub")
			if _, err = os.Stat(legacyConfig); err == nil {
				ui.Errorf("Notice: config file found but not respected at: %s\n", legacyConfig)
				ui.Errorf("You might want to move it to `%s' to avoid re-authenticating.\n", configFile)
			}
		}
J
Jingwen Owen Ou 已提交
295 296
	}

297
	return configFile, nil
J
Jingwen Owen Ou 已提交
298 299
}

300 301 302
var currentConfig *Config
var configLoadedFrom = ""

303
func CurrentConfig() *Config {
304 305 306 307 308 309
	filename := configsFile()
	if configLoadedFrom != filename {
		currentConfig = &Config{}
		newConfigService().Load(filename, currentConfig)
		configLoadedFrom = filename
	}
J
Jingwen Owen Ou 已提交
310

311
	return currentConfig
J
Jingwen Owen Ou 已提交
312
}
313

314
func (c *Config) DefaultHost() (host *Host, err error) {
315
	if GitHubHostEnv != "" {
316
		host, err = c.PromptForHost(GitHubHostEnv)
J
Jingwen Owen Ou 已提交
317
	} else if len(c.Hosts) > 0 {
318
		host = c.selectHost()
319 320
		// HACK: forces host to inherit GITHUB_TOKEN if applicable
		host, err = c.PromptForHost(host.Host)
321
	} else {
322
		host, err = c.PromptForHost(DefaultGitHubHost())
323 324 325 326 327
	}

	return
}

G
Goel 已提交
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
// CheckWriteable checks if config file is writeable. This should
// be called before asking for credentials and only if current
// operation needs to update the file. See issue #1314 for details.
func CheckWriteable(filename string) error {
	// Check if file exists already. if it doesn't, we will delete it after
	// checking for writeabilty
	fileExistsAlready := false

	if _, err := os.Stat(filename); err == nil {
		fileExistsAlready = true
	}

	err := os.MkdirAll(filepath.Dir(filename), 0771)
	if err != nil {
		return err
	}

	w, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
	if err != nil {
		return err
	}
	w.Close()

	if !fileExistsAlready {
		err := os.Remove(filename)
		if err != nil {
			return err
		}
	}
	return nil
}

J
Jingwen Owen Ou 已提交
360
// Public for testing purpose
361
func CreateTestConfigs(user, token string) *Config {
362
	f, _ := ioutil.TempFile("", "test-config")
363
	os.Setenv("HUB_CONFIG", f.Name())
364

365
	host := &Host{
366 367 368
		User:        "jingweno",
		AccessToken: "123",
		Host:        GitHubHost,
369
	}
370

371
	c := &Config{Hosts: []*Host{host}}
372 373 374 375
	err := newConfigService().Save(f.Name(), c)
	if err != nil {
		panic(err)
	}
376 377 378

	return c
}