1package gitea
2
3import (
4 "context"
5 "fmt"
6 "path"
7 "regexp"
8 "sort"
9 "strings"
10
11 "github.com/pkg/errors"
12
13 "github.com/MichaelMure/git-bug/bridge/core"
14 "github.com/MichaelMure/git-bug/bridge/core/auth"
15 "github.com/MichaelMure/git-bug/cache"
16 "github.com/MichaelMure/git-bug/commands/input"
17 "github.com/MichaelMure/git-bug/repository"
18)
19
20var (
21 ErrBadProjectURL = errors.New("bad project url")
22)
23
24func (g *Gitea) ValidParams() map[string]interface{} {
25 return map[string]interface{}{
26 "URL": nil,
27 "Login": nil,
28 "CredPrefix": nil,
29 "TokenRaw": nil,
30 }
31}
32
33func (g *Gitea) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
34 var err error
35 var baseURL, owner, project string
36
37 // get project url
38 switch {
39 case params.URL != "":
40 baseURL, owner, project, err = splitURL(params.URL)
41 if err != nil {
42 return nil, err
43 }
44 default:
45 // terminal prompt
46 if !interactive {
47 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitea project URL via the --url option.")
48 }
49 baseURL, owner, project, err = promptURL(repo)
50 if err != nil {
51 return nil, errors.Wrap(err, "url prompt")
52 }
53 }
54
55 var login string
56 var cred auth.Credential
57
58 switch {
59 case params.CredPrefix != "":
60 cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
61 if err != nil {
62 return nil, err
63 }
64 l, ok := cred.GetMetadata(auth.MetaKeyLogin)
65 if !ok {
66 return nil, fmt.Errorf("credential doesn't have a login")
67 }
68 login = l
69 case params.TokenRaw != "":
70 token := auth.NewToken(target, params.TokenRaw)
71 login, err = getLoginFromToken(baseURL, token)
72 if err != nil {
73 return nil, err
74 }
75 token.SetMetadata(auth.MetaKeyLogin, login)
76 token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
77 cred = token
78 default:
79 if params.Login == "" {
80 if !interactive {
81 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
82 }
83 // TODO: validate username
84 login, err = input.Prompt("Gitea login", "login", input.Required)
85 } else {
86 // TODO: validate username
87 login = params.Login
88 }
89 if err != nil {
90 return nil, err
91 }
92 if !interactive {
93 return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
94 }
95 cred, err = promptTokenOptions(repo, login, baseURL)
96 if err != nil {
97 return nil, err
98 }
99 }
100
101 token, ok := cred.(*auth.Token)
102 if !ok {
103 return nil, fmt.Errorf("the Gitea bridge only handle token credentials")
104 }
105
106 // verify access to the repository with token
107 _, err = validateProject(baseURL, owner, project, token)
108 if err != nil {
109 return nil, errors.Wrap(err, "project validation")
110 }
111
112 conf := make(core.Configuration)
113 conf[core.ConfigKeyTarget] = target
114 conf[confKeyBaseURL] = baseURL
115 conf[confKeyOwner] = owner
116 conf[confKeyProject] = project
117 conf[confKeyDefaultLogin] = login
118
119 err = g.ValidateConfig(conf)
120 if err != nil {
121 return nil, err
122 }
123
124 // don't forget to store the now known valid token
125 if !auth.IdExist(repo, cred.ID()) {
126 err = auth.Store(repo, cred)
127 if err != nil {
128 return nil, err
129 }
130 }
131
132 return conf, core.FinishConfig(repo, metaKeyGiteaLogin, login)
133}
134
135func (g *Gitea) ValidateConfig(conf core.Configuration) error {
136 if v, ok := conf[core.ConfigKeyTarget]; !ok {
137 return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
138 } else if v != target {
139 return fmt.Errorf("unexpected target name: %v", v)
140 }
141 if _, ok := conf[confKeyBaseURL]; !ok {
142 return fmt.Errorf("missing %s key", confKeyBaseURL)
143 }
144 if _, ok := conf[confKeyOwner]; !ok {
145 return fmt.Errorf("missing %s key", confKeyOwner)
146 }
147 if _, ok := conf[confKeyProject]; !ok {
148 return fmt.Errorf("missing %s key", confKeyProject)
149 }
150 if _, ok := conf[confKeyDefaultLogin]; !ok {
151 return fmt.Errorf("missing %s key", confKeyDefaultLogin)
152 }
153
154 return nil
155}
156
157func promptTokenOptions(repo repository.RepoKeyring, login, baseURL string) (auth.Credential, error) {
158 creds, err := auth.List(repo,
159 auth.WithTarget(target),
160 auth.WithKind(auth.KindToken),
161 auth.WithMeta(auth.MetaKeyLogin, login),
162 auth.WithMeta(auth.MetaKeyBaseURL, baseURL),
163 )
164 if err != nil {
165 return nil, err
166 }
167
168 cred, index, err := input.PromptCredential(target, "token", creds, []string{
169 "enter my token",
170 })
171 switch {
172 case err != nil:
173 return nil, err
174 case cred != nil:
175 return cred, nil
176 case index == 0:
177 return promptToken(baseURL)
178 default:
179 panic("missed case")
180 }
181}
182
183func promptToken(baseURL string) (*auth.Token, error) {
184 fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseURL, "user/settings/applications"))
185 fmt.Println()
186
187 re := regexp.MustCompile(`^[a-z0-9]{40}$`)
188
189 var login string
190
191 validator := func(name string, value string) (complaint string, err error) {
192 if !re.MatchString(value) {
193 return "token has incorrect format", nil
194 }
195 login, err = getLoginFromToken(baseURL, auth.NewToken(target, value))
196 if err != nil {
197 return fmt.Sprintf("token is invalid: %v", err), nil
198 }
199 return "", nil
200 }
201
202 rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
203 if err != nil {
204 return nil, err
205 }
206
207 token := auth.NewToken(target, rawToken)
208 token.SetMetadata(auth.MetaKeyLogin, login)
209 token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
210
211 return token, nil
212}
213
214func promptURL(repo repository.RepoCommon) (string, string, string, error) {
215 validRemotes, err := getRemoteURLs(repo)
216 if err != nil {
217 return "", "", "", err
218 }
219
220 validator := func(name, value string) (string, error) {
221 _, _, _, err := splitURL(value)
222 if err != nil {
223 return err.Error(), nil
224 }
225 return "", nil
226 }
227
228 url, err := input.PromptURLWithRemote("Gitea project URL", "URL", validRemotes, input.Required, input.IsURL, validator)
229 if err != nil {
230 return "", "", "", err
231 }
232
233 return splitURL(url)
234}
235
236func splitURL(url string) (baseURL, owner, project string, err error) {
237 cleanURL := strings.TrimSuffix(url, ".git")
238
239 re := regexp.MustCompile(`(.*)/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)$`)
240
241 res := re.FindStringSubmatch(cleanURL)
242 if res == nil {
243 return "", "", "", ErrBadProjectURL
244 }
245
246 baseURL = res[1]
247 owner = res[2]
248 project = res[3]
249 return
250}
251
252func getRemoteURLs(repo repository.RepoCommon) ([]string, error) {
253 remotes, err := repo.GetRemotes()
254 if err != nil {
255 return nil, err
256 }
257
258 urls := make([]string, 0, len(remotes))
259 for _, url := range remotes {
260 urls = append(urls, url)
261 }
262
263 sort.Strings(urls)
264
265 return urls, nil
266}
267
268func validateProject(baseURL, owner, project string, token *auth.Token) (bool, error) {
269 client, err := buildClient(baseURL, token)
270 if err != nil {
271 return false, err
272 }
273
274 ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
275 defer cancel()
276 client.SetContext(ctx)
277
278 _, _, err = client.GetRepo(owner, project)
279 if err != nil {
280 return false, errors.Wrap(err, "wrong token scope or non-existent project")
281 }
282
283 return true, nil
284}
285
286func getLoginFromToken(baseURL string, token *auth.Token) (string, error) {
287 client, err := buildClient(baseURL, token)
288 if err != nil {
289 return "", err
290 }
291
292 ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
293 defer cancel()
294 client.SetContext(ctx)
295
296 user, _, err := client.GetMyUserInfo()
297 if err != nil {
298 return "", err
299 }
300 if user.UserName == "" {
301 return "", fmt.Errorf("gitea say username is empty")
302 }
303
304 return user.UserName, nil
305}