transportcontroller.go (12807B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package testrig 19 20 import ( 21 "bytes" 22 "encoding/json" 23 "encoding/xml" 24 "io" 25 "net/http" 26 "strings" 27 "sync" 28 29 "github.com/superseriousbusiness/activity/streams" 30 "github.com/superseriousbusiness/activity/streams/vocab" 31 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 32 "github.com/superseriousbusiness/gotosocial/internal/federation" 33 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 34 "github.com/superseriousbusiness/gotosocial/internal/httpclient" 35 "github.com/superseriousbusiness/gotosocial/internal/log" 36 "github.com/superseriousbusiness/gotosocial/internal/state" 37 "github.com/superseriousbusiness/gotosocial/internal/transport" 38 ) 39 40 const ( 41 applicationJSON = "application/json" 42 applicationActivityJSON = "application/activity+json" 43 ) 44 45 // NewTestTransportController returns a test transport controller with the given http client. 46 // 47 // Obviously for testing purposes you should not be making actual http calls to other servers. 48 // To obviate this, use the function NewMockHTTPClient in this package to return a mock http 49 // client that doesn't make any remote calls but just returns whatever you tell it to. 50 // 51 // Unlike the other test interfaces provided in this package, you'll probably want to call this function 52 // PER TEST rather than per suite, so that the do function can be set on a test by test (or even more granular) 53 // basis. 54 func NewTestTransportController(state *state.State, client httpclient.SigningClient) transport.Controller { 55 return transport.NewController(state, NewTestFederatingDB(state), &federation.Clock{}, client) 56 } 57 58 type MockHTTPClient struct { 59 do func(req *http.Request) (*http.Response, error) 60 61 TestRemoteStatuses map[string]vocab.ActivityStreamsNote 62 TestRemotePeople map[string]vocab.ActivityStreamsPerson 63 TestRemoteGroups map[string]vocab.ActivityStreamsGroup 64 TestRemoteServices map[string]vocab.ActivityStreamsService 65 TestRemoteAttachments map[string]RemoteAttachmentFile 66 TestRemoteEmojis map[string]vocab.TootEmoji 67 TestTombstones map[string]*gtsmodel.Tombstone 68 69 SentMessages sync.Map 70 } 71 72 // NewMockHTTPClient returns a client that conforms to the pub.HttpClient interface. 73 // 74 // If do is nil, then a standard response set will be mocked out, which includes models stored in the 75 // testrig, and webfinger responses as well. 76 // 77 // If do is not nil, then the given do function will always be used, which allows callers 78 // to customize how the client is mocked. 79 // 80 // Note that you should never ever make ACTUAL http calls with this thing. 81 func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relativeMediaPath string) *MockHTTPClient { 82 mockHTTPClient := &MockHTTPClient{} 83 84 if do != nil { 85 mockHTTPClient.do = do 86 return mockHTTPClient 87 } 88 89 mockHTTPClient.TestRemoteStatuses = NewTestFediStatuses() 90 mockHTTPClient.TestRemotePeople = NewTestFediPeople() 91 mockHTTPClient.TestRemoteGroups = NewTestFediGroups() 92 mockHTTPClient.TestRemoteServices = NewTestFediServices() 93 mockHTTPClient.TestRemoteAttachments = NewTestFediAttachments(relativeMediaPath) 94 mockHTTPClient.TestRemoteEmojis = NewTestFediEmojis() 95 mockHTTPClient.TestTombstones = NewTestTombstones() 96 97 mockHTTPClient.do = func(req *http.Request) (*http.Response, error) { 98 responseCode := http.StatusNotFound 99 responseBytes := []byte(`{"error":"404 not found"}`) 100 responseContentType := applicationJSON 101 responseContentLength := len(responseBytes) 102 103 if req.Method == http.MethodPost { 104 b, err := io.ReadAll(req.Body) 105 if err != nil { 106 panic(err) 107 } 108 109 if sI, loaded := mockHTTPClient.SentMessages.LoadOrStore(req.URL.String(), [][]byte{b}); loaded { 110 s, ok := sI.([][]byte) 111 if !ok { 112 panic("SentMessages entry wasn't [][]byte") 113 } 114 s = append(s, b) 115 mockHTTPClient.SentMessages.Store(req.URL.String(), s) 116 } 117 118 responseCode = http.StatusOK 119 responseBytes = []byte(`{"ok":"accepted"}`) 120 responseContentType = applicationJSON 121 responseContentLength = len(responseBytes) 122 } else if strings.Contains(req.URL.String(), ".well-known/webfinger") { 123 responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req) 124 } else if strings.Contains(req.URL.String(), ".weird-webfinger-location/webfinger") { 125 responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req) 126 } else if strings.Contains(req.URL.String(), ".well-known/host-meta") { 127 responseCode, responseBytes, responseContentType, responseContentLength = HostMetaResponse(req) 128 } else if note, ok := mockHTTPClient.TestRemoteStatuses[req.URL.String()]; ok { 129 // the request is for a note that we have stored 130 noteI, err := streams.Serialize(note) 131 if err != nil { 132 panic(err) 133 } 134 noteJSON, err := json.Marshal(noteI) 135 if err != nil { 136 panic(err) 137 } 138 responseCode = http.StatusOK 139 responseBytes = noteJSON 140 responseContentType = applicationActivityJSON 141 responseContentLength = len(noteJSON) 142 } else if person, ok := mockHTTPClient.TestRemotePeople[req.URL.String()]; ok { 143 // the request is for a person that we have stored 144 personI, err := streams.Serialize(person) 145 if err != nil { 146 panic(err) 147 } 148 personJSON, err := json.Marshal(personI) 149 if err != nil { 150 panic(err) 151 } 152 responseCode = http.StatusOK 153 responseBytes = personJSON 154 responseContentType = applicationActivityJSON 155 responseContentLength = len(personJSON) 156 } else if group, ok := mockHTTPClient.TestRemoteGroups[req.URL.String()]; ok { 157 // the request is for a person that we have stored 158 groupI, err := streams.Serialize(group) 159 if err != nil { 160 panic(err) 161 } 162 groupJSON, err := json.Marshal(groupI) 163 if err != nil { 164 panic(err) 165 } 166 responseCode = http.StatusOK 167 responseBytes = groupJSON 168 responseContentType = applicationActivityJSON 169 responseContentLength = len(groupJSON) 170 } else if service, ok := mockHTTPClient.TestRemoteServices[req.URL.String()]; ok { 171 serviceI, err := streams.Serialize(service) 172 if err != nil { 173 panic(err) 174 } 175 serviceJSON, err := json.Marshal(serviceI) 176 if err != nil { 177 panic(err) 178 } 179 responseCode = http.StatusOK 180 responseBytes = serviceJSON 181 responseContentType = applicationActivityJSON 182 responseContentLength = len(serviceJSON) 183 } else if emoji, ok := mockHTTPClient.TestRemoteEmojis[req.URL.String()]; ok { 184 emojiI, err := streams.Serialize(emoji) 185 if err != nil { 186 panic(err) 187 } 188 emojiJSON, err := json.Marshal(emojiI) 189 if err != nil { 190 panic(err) 191 } 192 responseCode = http.StatusOK 193 responseBytes = emojiJSON 194 responseContentType = applicationActivityJSON 195 responseContentLength = len(emojiJSON) 196 } else if attachment, ok := mockHTTPClient.TestRemoteAttachments[req.URL.String()]; ok { 197 responseCode = http.StatusOK 198 responseBytes = attachment.Data 199 responseContentType = attachment.ContentType 200 responseContentLength = len(attachment.Data) 201 } else if _, ok := mockHTTPClient.TestTombstones[req.URL.String()]; ok { 202 responseCode = http.StatusGone 203 responseBytes = []byte{} 204 responseContentType = "text/html" 205 responseContentLength = 0 206 } 207 208 log.Debugf(nil, "returning response %s", string(responseBytes)) 209 reader := bytes.NewReader(responseBytes) 210 readCloser := io.NopCloser(reader) 211 return &http.Response{ 212 Request: req, 213 StatusCode: responseCode, 214 Body: readCloser, 215 ContentLength: int64(responseContentLength), 216 Header: http.Header{ 217 "content-type": {responseContentType}, 218 }, 219 }, nil 220 } 221 222 return mockHTTPClient 223 } 224 225 func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { 226 return m.do(req) 227 } 228 229 func (m *MockHTTPClient) DoSigned(req *http.Request, sign httpclient.SignFunc) (*http.Response, error) { 230 return m.do(req) 231 } 232 233 func HostMetaResponse(req *http.Request) (responseCode int, responseBytes []byte, responseContentType string, responseContentLength int) { 234 var hm *apimodel.HostMeta 235 236 if req.URL.String() == "https://misconfigured-instance.com/.well-known/host-meta" { 237 hm = &apimodel.HostMeta{ 238 XMLNS: "http://docs.oasis-open.org/ns/xri/xrd-1.0", 239 Link: []apimodel.Link{ 240 { 241 Rel: "lrdd", 242 Type: "application/xrd+xml", 243 Template: "https://misconfigured-instance.com/.weird-webfinger-location/webfinger?resource={uri}", 244 }, 245 }, 246 } 247 } 248 249 if hm == nil { 250 log.Debugf(nil, "hostmeta response not available for %s", req.URL) 251 responseCode = http.StatusNotFound 252 responseBytes = []byte(``) 253 responseContentType = "application/xml" 254 responseContentLength = len(responseBytes) 255 return 256 } 257 258 hmXML, err := xml.Marshal(hm) 259 if err != nil { 260 panic(err) 261 } 262 responseCode = http.StatusOK 263 responseBytes = hmXML 264 responseContentType = "application/xml" 265 responseContentLength = len(hmXML) 266 return 267 } 268 269 func WebfingerResponse(req *http.Request) (responseCode int, responseBytes []byte, responseContentType string, responseContentLength int) { 270 var wfr *apimodel.WellKnownResponse 271 272 switch req.URL.String() { 273 case "https://unknown-instance.com/.well-known/webfinger?resource=acct%3Asome_group%40unknown-instance.com": 274 wfr = &apimodel.WellKnownResponse{ 275 Subject: "acct:some_group@unknown-instance.com", 276 Links: []apimodel.Link{ 277 { 278 Rel: "self", 279 Type: applicationActivityJSON, 280 Href: "https://unknown-instance.com/groups/some_group", 281 }, 282 }, 283 } 284 case "https://owncast.example.org/.well-known/webfinger?resource=acct%3Argh%40owncast.example.org": 285 wfr = &apimodel.WellKnownResponse{ 286 Subject: "acct:rgh@example.org", 287 Links: []apimodel.Link{ 288 { 289 Rel: "self", 290 Type: applicationActivityJSON, 291 Href: "https://owncast.example.org/federation/user/rgh", 292 }, 293 }, 294 } 295 case "https://unknown-instance.com/.well-known/webfinger?resource=acct%3Abrand_new_person%40unknown-instance.com": 296 wfr = &apimodel.WellKnownResponse{ 297 Subject: "acct:brand_new_person@unknown-instance.com", 298 Links: []apimodel.Link{ 299 { 300 Rel: "self", 301 Type: applicationActivityJSON, 302 Href: "https://unknown-instance.com/users/brand_new_person", 303 }, 304 }, 305 } 306 case "https://turnip.farm/.well-known/webfinger?resource=acct%3Aturniplover6969%40turnip.farm": 307 wfr = &apimodel.WellKnownResponse{ 308 Subject: "acct:turniplover6969@turnip.farm", 309 Links: []apimodel.Link{ 310 { 311 Rel: "self", 312 Type: applicationActivityJSON, 313 Href: "https://turnip.farm/users/turniplover6969", 314 }, 315 }, 316 } 317 case "https://fossbros-anonymous.io/.well-known/webfinger?resource=acct%3Afoss_satan%40fossbros-anonymous.io": 318 wfr = &apimodel.WellKnownResponse{ 319 Subject: "acct:foss_satan@fossbros-anonymous.io", 320 Links: []apimodel.Link{ 321 { 322 Rel: "self", 323 Type: applicationActivityJSON, 324 Href: "http://fossbros-anonymous.io/users/foss_satan", 325 }, 326 }, 327 } 328 case "https://example.org/.well-known/webfinger?resource=acct%3ASome_User%40example.org": 329 wfr = &apimodel.WellKnownResponse{ 330 Subject: "acct:Some_User@example.org", 331 Links: []apimodel.Link{ 332 { 333 Rel: "self", 334 Type: applicationActivityJSON, 335 Href: "https://example.org/users/Some_User", 336 }, 337 }, 338 } 339 case "https://misconfigured-instance.com/.weird-webfinger-location/webfinger?resource=acct%3Asomeone%40misconfigured-instance.com": 340 wfr = &apimodel.WellKnownResponse{ 341 Subject: "acct:someone@misconfigured-instance.com", 342 Links: []apimodel.Link{ 343 { 344 Rel: "self", 345 Type: applicationActivityJSON, 346 Href: "https://misconfigured-instance.com/users/someone", 347 }, 348 }, 349 } 350 } 351 352 if wfr == nil { 353 log.Debugf(nil, "webfinger response not available for %s", req.URL) 354 responseCode = http.StatusNotFound 355 responseBytes = []byte(`{"error":"not found"}`) 356 responseContentType = applicationJSON 357 responseContentLength = len(responseBytes) 358 return 359 } 360 361 wfrJSON, err := json.Marshal(wfr) 362 if err != nil { 363 panic(err) 364 } 365 responseCode = http.StatusOK 366 responseBytes = wfrJSON 367 responseContentType = applicationJSON 368 responseContentLength = len(wfrJSON) 369 return 370 }