-
Notifications
You must be signed in to change notification settings - Fork 23
/
form.go
173 lines (155 loc) · 5.21 KB
/
form.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// Package form enables interactive login without using a web browser.
package form
import (
"context"
"net/url"
"golang.org/x/net/publicsuffix"
"gopkg.in/errgo.v1"
"gopkg.in/httprequest.v1"
"gopkg.in/juju/environschema.v1"
"gopkg.in/juju/environschema.v1/form"
"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
)
/*
PROTOCOL
A form login works as follows:
Client Login Service
| |
| Discharge request |
|----------------------------------->|
| |
| Interaction-required error with |
| "form" entry with formURL. |
|<-----------------------------------|
| |
| GET "form" URL |
|----------------------------------->|
| |
| Schema definition |
|<-----------------------------------|
| |
+-------------+ |
| Client | |
| Interaction | |
+-------------+ |
| |
| POST data to "form" URL |
|----------------------------------->|
| |
| Form login response |
| with discharge token |
|<-----------------------------------|
| |
| Discharge request with |
| discharge token. |
|----------------------------------->|
| |
| Discharge macaroon |
|<-----------------------------------|
The schema is provided as a environschema.Fields object. It is the
client's responsibility to interpret the schema and present it to the
user.
*/
const (
// InteractionMethod is the methodURLs key
// used for a URL that can be used for form-based
// interaction.
InteractionMethod = "form"
)
// SchemaResponse contains the message expected in response to the schema
// request.
type SchemaResponse struct {
Schema environschema.Fields `json:"schema"`
}
// InteractionInfo holds the information expected in
// the form interaction entry in an interaction-required
// error.
type InteractionInfo struct {
URL string `json:"url"`
}
// LoginRequest is a request to perform a login using the provided form.
type LoginRequest struct {
httprequest.Route `httprequest:"POST"`
Body LoginBody `httprequest:",body"`
}
// LoginBody holds the body of a form login request.
type LoginBody struct {
Form map[string]interface{} `json:"form"`
}
type LoginResponse struct {
Token *httpbakery.DischargeToken `json:"token"`
}
// Interactor implements httpbakery.Interactor
// by providing form-based interaction.
type Interactor struct {
// Filler holds the form filler that will be used when
// form-based interaction is required.
Filler form.Filler
}
// Kind implements httpbakery.Interactor.Kind.
func (i Interactor) Kind() string {
return InteractionMethod
}
// Interact implements httpbakery.Interactor.Interact.
func (i Interactor) Interact(ctx context.Context, client *httpbakery.Client, location string, interactionRequiredErr *httpbakery.Error) (*httpbakery.DischargeToken, error) {
var p InteractionInfo
if err := interactionRequiredErr.InteractionMethod(InteractionMethod, &p); err != nil {
return nil, errgo.Mask(err)
}
if p.URL == "" {
return nil, errgo.Newf("no URL found in form information")
}
schemaURL, err := relativeURL(location, p.URL)
if err != nil {
return nil, errgo.Notef(err, "invalid url %q", p.URL)
}
httpReqClient := &httprequest.Client{
Doer: client,
}
var s SchemaResponse
if err := httpReqClient.Get(ctx, schemaURL.String(), &s); err != nil {
return nil, errgo.Notef(err, "cannot get schema")
}
if len(s.Schema) == 0 {
return nil, errgo.Newf("invalid schema: no fields found")
}
host, err := publicsuffix.EffectiveTLDPlusOne(schemaURL.Host)
if err != nil {
host = schemaURL.Host
}
formValues, err := i.Filler.Fill(form.Form{
Title: "Log in to " + host,
Fields: s.Schema,
})
if err != nil {
return nil, errgo.NoteMask(err, "cannot handle form", errgo.Any)
}
lr := LoginRequest{
Body: LoginBody{
Form: formValues,
},
}
var lresp LoginResponse
if err := httpReqClient.CallURL(ctx, schemaURL.String(), &lr, &lresp); err != nil {
return nil, errgo.Notef(err, "cannot submit form")
}
if lresp.Token == nil {
return nil, errgo.Newf("no token found in form response")
}
return lresp.Token, nil
}
// relativeURL returns newPath relative to an original URL.
func relativeURL(base, new string) (*url.URL, error) {
if new == "" {
return nil, errgo.Newf("empty URL")
}
baseURL, err := url.Parse(base)
if err != nil {
return nil, errgo.Notef(err, "cannot parse URL")
}
newURL, err := url.Parse(new)
if err != nil {
return nil, errgo.Notef(err, "cannot parse URL")
}
return baseURL.ResolveReference(newURL), nil
}