-
Notifications
You must be signed in to change notification settings - Fork 0
/
autodiscover.go
237 lines (198 loc) · 6.17 KB
/
autodiscover.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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package autodiscover
import (
"bytes"
"crypto/tls"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)
type DiscoveredInfo struct {
EWSUrl string
ExchangeVersion string
}
func redirectedUrl(emailAddress string, password string) string {
domain := strings.Split(emailAddress, "@")[1]
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
httpClient := &http.Client{
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}}
req, err := http.NewRequest("GET", fmt.Sprintf("http://autodiscover.%s/autodiscover/autodiscover.xml", domain), nil)
if err != nil {
return ""
}
req.SetBasicAuth(emailAddress, password)
resp, err := httpClient.Do(req)
if err != nil || resp.StatusCode != 302 {
return ""
}
return resp.Header.Get("Location")
}
func discoveryUrls(emailAddress string, password string) [3]string {
domain := strings.Split(emailAddress, "@")[1]
// TODO: also add higher level domain(s)
return [3]string{
fmt.Sprintf("https://%s/autodiscover/autodiscover.xml", domain),
fmt.Sprintf("https://autodiscover.%s/autodiscover/autodiscover.xml", domain),
redirectedUrl(emailAddress, password),
}
}
type AutoDiscoveryRequest struct {
XMLName xml.Name `xml:"http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006 Autodiscover"`
EMailAddress string `xml:"Request>EMailAddress"`
AcceptableResponseSchema string `xml:"Request>AcceptableResponseSchema"`
}
func requestBody(emailAddress string) ([]byte, error) {
requestBody := AutoDiscoveryRequest{
EMailAddress: emailAddress,
AcceptableResponseSchema: "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a",
}
return xml.Marshal(&requestBody)
}
type AutoDiscoveryResponse struct {
XMLName xml.Name `xml:"http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006 Autodiscover"`
Protocols []AutoDiscoveryProtocol `xml:"Response>Account>Protocol"`
}
type AutoDiscoveryProtocol struct {
XMLName xml.Name `xml:"Protocol"`
Type string `xml:"Type"`
ServerVersion string `xml:"ServerVersion"`
EwsUrl string `xml:"EwsUrl"`
}
func ExchangeVersion(major int64, minor int64, build int64) (string, error) {
switch major {
case 8:
switch minor {
case 0:
return "Exchange2007", nil
case 1:
return "Exchange2007_SP1", nil
case 2:
return "Exchange2007_SP1", nil
case 3:
return "Exchange2007_SP1", nil
default:
return "", errors.New("unknown minor version")
}
case 14:
switch minor {
case 0:
return "Exchange2010", nil
case 1:
return "Exchange2010_SP1", nil
case 2:
return "Exchange2010_SP2", nil
case 3:
return "Exchange2010_SP2", nil
default:
return "", errors.New("unknown minor version")
}
case 15:
switch minor {
case 0:
if build >= 847 {
return "Exchange2013_SP1", nil // Minor builds starting from 847 are Exchange2013_SP1
} else {
return "Exchange2013", nil
}
case 1:
return "Exchange2016", nil
case 2:
return "Exchange2019", nil
case 20:
return "Exchange2016", nil // This is Office365
default:
return "", errors.New("unknown minor version")
}
default:
return "", errors.New("unknown major version")
}
}
func exchangeVersion(rawHex string) (string, error) {
i, err := strconv.ParseUint(rawHex, 16, 32)
if err != nil {
return "", err
}
binString := fmt.Sprintf("%032b", i)
major, _ := strconv.ParseInt(binString[4:10], 2, 32)
minor, _ := strconv.ParseInt(binString[10:16], 2, 32)
build, _ := strconv.ParseInt(binString[17:32], 2, 32)
return ExchangeVersion(major, minor, build)
}
func parseResponse(body []byte) (DiscoveredInfo, error) {
var response AutoDiscoveryResponse
err := xml.Unmarshal(body, &response)
if err != nil {
return DiscoveredInfo{}, err
}
protocols := response.Protocols
exchangeProtocolIndex := slices.IndexFunc(protocols, func(p AutoDiscoveryProtocol) bool { return p.Type == "EXCH" })
if exchangeProtocolIndex < 0 {
return DiscoveredInfo{}, errors.New("failed to find Exchange protocol in response")
}
exchangeProtocol := protocols[exchangeProtocolIndex]
ewsUrl := exchangeProtocol.EwsUrl
rawVersion := exchangeProtocol.ServerVersion
if ewsUrl == "" {
expressProtocolIndex := slices.IndexFunc(protocols, func(p AutoDiscoveryProtocol) bool { return p.Type == "EXPR" })
if expressProtocolIndex < 0 {
return DiscoveredInfo{}, errors.New("failed to find Express protocol in response")
}
expressProtocol := protocols[expressProtocolIndex]
ewsUrl = expressProtocol.EwsUrl
}
exchangeVersion, err := exchangeVersion(rawVersion)
if err != nil {
return DiscoveredInfo{}, err
}
return DiscoveredInfo{EWSUrl: ewsUrl, ExchangeVersion: exchangeVersion}, nil
}
func Discover(emailAddress string, password string) (DiscoveredInfo, error) {
if !strings.Contains(emailAddress, "@") {
return DiscoveredInfo{}, errors.New("username must be an email address for auto-discovery")
}
transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
httpClient := &http.Client{Transport: transport}
body, err := requestBody(emailAddress)
if err != nil {
return DiscoveredInfo{}, err
}
for _, url := range discoveryUrls(emailAddress, password) {
if url == "" {
continue
}
req, err := http.NewRequest("GET", url, bytes.NewBuffer(body))
if err != nil {
log.WithError(err).Warn("failed to create GET request for " + url)
continue
}
req.SetBasicAuth(emailAddress, password)
resp, err := httpClient.Do(req)
if err != nil {
log.WithError(err).Warn("failed to receive response from " + url)
continue
}
if resp.StatusCode != 200 {
continue
}
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
log.WithError(err).Warn("failed to read response from " + url)
continue
}
info, parseErr := parseResponse(body)
if parseErr != nil {
log.WithError(err).Warn("failed to parse response from " + url)
continue
}
return info, nil
}
return DiscoveredInfo{}, errors.New("failed to fetch discovery information")
}