func TestRequestHandlerSend(t *testing.T) { c := &config.Config{ NetworkPassphrase: "Test SDF Network ; September 2015", Keys: config.Keys{ // GBYJZW5XFAI6XV73H5SAIUYK6XZI4CGGVBUBO3ANA2SV7KKDAXTV6AEB SigningSeed: "SDWTLFPALQSP225BSMX7HPZ7ZEAYSUYNDLJ5QI3YGVBNRUIIELWH3XUV", }, Callbacks: config.Callbacks{ FetchInfo: "http://fetch_info", }, } mockHTTPClient := new(mocks.MockHTTPClient) mockEntityManager := new(mocks.MockEntityManager) mockRepository := new(mocks.MockRepository) mockFederationResolver := new(mocks.MockFederationResolver) mockSignerVerifier := new(mocks.MockSignerVerifier) mockStellartomlResolver := new(mocks.MockStellartomlResolver) requestHandler := RequestHandler{} // Inject mocks var g inject.Graph err := g.Provide( &inject.Object{Value: &requestHandler}, &inject.Object{Value: c}, &inject.Object{Value: mockHTTPClient}, &inject.Object{Value: mockEntityManager}, &inject.Object{Value: mockRepository}, &inject.Object{Value: mockFederationResolver}, &inject.Object{Value: mockSignerVerifier}, &inject.Object{Value: mockStellartomlResolver}, ) if err != nil { panic(err) } if err := g.Populate(); err != nil { panic(err) } httpHandle := func(w http.ResponseWriter, r *http.Request) { requestHandler.HandlerSend(web.C{}, w, r) } testServer := httptest.NewServer(http.HandlerFunc(httpHandle)) defer testServer.Close() Convey("Given send request", t, func() { Convey("When source param is missing", func() { statusCode, response := net.GetResponse(testServer, url.Values{}) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) expected := test.StringToJSONMap(`{ "code": "missing_parameter", "message": "Required parameter is missing.", "data": { "name": "source" } }`) assert.Equal(t, expected, test.StringToJSONMap(responseString)) }) Convey("When source param is invalid", func() { params := url.Values{ "source": {"bad"}, "sender": {"alice*stellar.org"}, // GAW77Z6GPWXSODJOMF5L5BMX6VMYGEJRKUNBC2CZ725JTQZORK74HQQD "destination": {"bob*stellar.org"}, // GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE "amount": {"20"}, "asset_code": {"USD"}, "asset_issuer": {"GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE"}, "extra_memo": {"hello world"}, } statusCode, response := net.GetResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 400, statusCode) expected := test.StringToJSONMap(`{ "code": "invalid_parameter", "message": "Invalid parameter.", "data": { "name": "source" } }`) assert.Equal(t, expected, test.StringToJSONMap(responseString)) }) Convey("When params are valid", func() { params := url.Values{ "source": {"GAW77Z6GPWXSODJOMF5L5BMX6VMYGEJRKUNBC2CZ725JTQZORK74HQQD"}, "sender": {"alice*stellar.org"}, // GAW77Z6GPWXSODJOMF5L5BMX6VMYGEJRKUNBC2CZ725JTQZORK74HQQD "destination": {"bob*stellar.org"}, // GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE "amount": {"20"}, "asset_code": {"USD"}, "asset_issuer": {"GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE"}, "extra_memo": {"hello world"}, } Convey("it returns SendResponse when success (payment)", func() { authServer := "https://acme.com/auth" mockFederationResolver.On( "Resolve", "bob*stellar.org", ).Return(federation.Response{ AccountID: "GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE", MemoType: "text", Memo: "bob", }, stellartoml.StellarToml{ AuthServer: authServer, }, nil).Once() transactionXdr := "AAAAAC3/58Z9rycNLmF6voWX9VmDETFVGhFoWf66mcMuir/DAAAAZAAAAAAAAAAAAAAAAAAAAANLw6drH5OZQsQFOcJZvFBrx8zFYFlryZWs/9cwBTVH5QAAAAEAAAAAAAAAAQAAAAAZUvzcMkXAfSwqbLoAiAlgPsZ7GIPRi7NIyKgEIBQ4nAAAAAFVU0QAAAAAABlS/NwyRcB9LCpsugCICWA+xnsYg9GLs0jIqAQgFDicAAAAAAvrwgAAAAAA" data := "{\"sender\":\"alice*stellar.org\",\"need_info\":false,\"tx\":\"" + transactionXdr + "\",\"memo\":\"{\\n \\\"transaction\\\": {\\n \\\"sender_info\\\": \\\"{\\\\\\\"name\\\\\\\": \\\\\\\"Alice Doe\\\\\\\"}\\\",\\n \\\"route\\\": \\\"bob\\\",\\n \\\"extra\\\": \\\"hello world\\\",\\n \\\"note\\\": \\\"\\\"\\n },\\n \\\"operations\\\": null\\n}\"}" sig := "YeMlOYWNysyGBfsAe40z9dGgpRsKSQrqFIGAEsyJQ8osnXlLPynvJ2WQDGcBq2n5AA96YZdABhQz5ymqvxfQDw==" authResponse := compliance.AuthResponse{ InfoStatus: compliance.AuthStatusOk, TxStatus: compliance.AuthStatusOk, } mockHTTPClient.On( "PostForm", c.Callbacks.FetchInfo, url.Values{"address": {"alice*stellar.org"}}, ).Return( net.BuildHTTPResponse(200, "{\"name\": \"Alice Doe\"}"), nil, ).Once() mockHTTPClient.On( "PostForm", authServer, url.Values{"data": {data}, "sig": {sig}}, ).Return( net.BuildHTTPResponse(200, string(authResponse.Marshal())), nil, ).Once() mockSignerVerifier.On( "Sign", c.Keys.SigningSeed, []byte(data), ).Return(sig, nil).Once() statusCode, response := net.GetResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 200, statusCode) expected := test.StringToJSONMap(`{ "auth_response": { "info_status": "ok", "tx_status": "ok" }, "transaction_xdr": "AAAAAC3/58Z9rycNLmF6voWX9VmDETFVGhFoWf66mcMuir/DAAAAZAAAAAAAAAAAAAAAAAAAAANLw6drH5OZQsQFOcJZvFBrx8zFYFlryZWs/9cwBTVH5QAAAAEAAAAAAAAAAQAAAAAZUvzcMkXAfSwqbLoAiAlgPsZ7GIPRi7NIyKgEIBQ4nAAAAAFVU0QAAAAAABlS/NwyRcB9LCpsugCICWA+xnsYg9GLs0jIqAQgFDicAAAAAAvrwgAAAAAA" }`) assert.Equal(t, expected, test.StringToJSONMap(responseString)) }) Convey("it returns SendResponse when success (path payment)", func() { params["send_max"] = []string{"100"} params["send_asset_code"] = []string{"USD"} params["send_asset_issuer"] = []string{"GBDOSO3K4JTGSWJSIHXAOFIBMAABVM3YK3FI6VJPKIHHM56XAFIUCGD6"} // Native params["path[0][asset_code]"] = []string{""} params["path[0][asset_issuer]"] = []string{""} // Credit params["path[1][asset_code]"] = []string{"EUR"} params["path[1][asset_issuer]"] = []string{"GAF3PBFQLH57KPECN4GRGHU5NUZ3XXKYYWLOTBIRJMBYHPUBWANIUCZU"} authServer := "https://acme.com/auth" mockFederationResolver.On( "Resolve", "bob*stellar.org", ).Return(federation.Response{ AccountID: "GAMVF7G4GJC4A7JMFJWLUAEIBFQD5RT3DCB5DC5TJDEKQBBACQ4JZVEE", MemoType: "text", Memo: "bob", }, stellartoml.StellarToml{ AuthServer: authServer, }, nil).Once() transactionXdr := "AAAAAC3/58Z9rycNLmF6voWX9VmDETFVGhFoWf66mcMuir/DAAAAZAAAAAAAAAAAAAAAAAAAAANLw6drH5OZQsQFOcJZvFBrx8zFYFlryZWs/9cwBTVH5QAAAAEAAAAAAAAAAgAAAAFVU0QAAAAAAEbpO2riZmlZMkHuBxUBYAAas3hWyo9VL1IOdnfXAVFBAAAAADuaygAAAAAAGVL83DJFwH0sKmy6AIgJYD7GexiD0YuzSMioBCAUOJwAAAABVVNEAAAAAAAZUvzcMkXAfSwqbLoAiAlgPsZ7GIPRi7NIyKgEIBQ4nAAAAAAL68IAAAAAAgAAAAAAAAABRVVSAAAAAAALt4SwWfv1PIJvDRMenW0zu91YxZbphRFLA4O+gbAaigAAAAA=" data := "{\"sender\":\"alice*stellar.org\",\"need_info\":false,\"tx\":\"" + transactionXdr + "\",\"memo\":\"{\\n \\\"transaction\\\": {\\n \\\"sender_info\\\": \\\"{\\\\\\\"name\\\\\\\": \\\\\\\"Alice Doe\\\\\\\"}\\\",\\n \\\"route\\\": \\\"bob\\\",\\n \\\"extra\\\": \\\"hello world\\\",\\n \\\"note\\\": \\\"\\\"\\n },\\n \\\"operations\\\": null\\n}\"}" sig := "ACamNqa0dF8gf97URhFVKWSD7fmvZKc5At+8dCLM5ySR0HsHySF3G2WuwYP2nKjeqjKmu3U9Z3+u1P10w1KBCA==" authResponse := compliance.AuthResponse{ InfoStatus: compliance.AuthStatusOk, TxStatus: compliance.AuthStatusOk, } mockHTTPClient.On( "PostForm", c.Callbacks.FetchInfo, url.Values{"address": {"alice*stellar.org"}}, ).Return( net.BuildHTTPResponse(200, "{\"name\": \"Alice Doe\"}"), nil, ).Once() mockHTTPClient.On( "PostForm", authServer, url.Values{"data": {data}, "sig": {sig}}, ).Return( net.BuildHTTPResponse(200, string(authResponse.Marshal())), nil, ).Once() mockSignerVerifier.On( "Sign", c.Keys.SigningSeed, []byte(data), ).Return(sig, nil).Once() statusCode, response := net.GetResponse(testServer, params) responseString := strings.TrimSpace(string(response)) assert.Equal(t, 200, statusCode) expected := test.StringToJSONMap(`{ "auth_response": { "info_status": "ok", "tx_status": "ok" }, "transaction_xdr": "AAAAAC3/58Z9rycNLmF6voWX9VmDETFVGhFoWf66mcMuir/DAAAAZAAAAAAAAAAAAAAAAAAAAANLw6drH5OZQsQFOcJZvFBrx8zFYFlryZWs/9cwBTVH5QAAAAEAAAAAAAAAAgAAAAFVU0QAAAAAAEbpO2riZmlZMkHuBxUBYAAas3hWyo9VL1IOdnfXAVFBAAAAADuaygAAAAAAGVL83DJFwH0sKmy6AIgJYD7GexiD0YuzSMioBCAUOJwAAAABVVNEAAAAAAAZUvzcMkXAfSwqbLoAiAlgPsZ7GIPRi7NIyKgEIBQ4nAAAAAAL68IAAAAAAgAAAAAAAAABRVVSAAAAAAALt4SwWfv1PIJvDRMenW0zu91YxZbphRFLA4O+gbAaigAAAAA=" }`) assert.Equal(t, expected, test.StringToJSONMap(responseString)) }) }) }) }
// HandlerAuth implements authorize endpoint func (rh *RequestHandler) HandlerAuth(c web.C, w http.ResponseWriter, r *http.Request) { authreq := &compliance.AuthRequest{} authreq.FromRequest(r) err := authreq.Validate() if err != nil { errorResponse := err.(*protocols.ErrorResponse) log.WithFields(errorResponse.LogData).Error(errorResponse.Error()) server.Write(w, errorResponse) return } var authData compliance.AuthData err = json.Unmarshal([]byte(authreq.Data), &authData) if err != nil { errorResponse := protocols.NewInvalidParameterError("data", authreq.Data) log.WithFields(errorResponse.LogData).Warn(errorResponse.Error()) server.Write(w, errorResponse) return } senderStellarToml, err := rh.StellarTomlResolver.GetStellarTomlByAddress(authData.Sender) if err != nil { log.WithFields(log.Fields{"err": err, "sender": authData.Sender}).Warn("Cannot get stellar.toml of sender") server.Write(w, protocols.InvalidParameterError) return } if senderStellarToml.SigningKey == "" { errorResponse := protocols.NewInvalidParameterError("data.sender", authData.Sender) log.WithFields(errorResponse.LogData).Warn("No SIGNING_KEY in stellar.toml of sender") server.Write(w, errorResponse) return } // Verify signature signatureBytes, err := base64.StdEncoding.DecodeString(authreq.Signature) if err != nil { errorResponse := protocols.NewInvalidParameterError("sig", authreq.Signature) log.WithFields(errorResponse.LogData).Warn("Error decoding signature") server.Write(w, errorResponse) return } err = rh.SignatureSignerVerifier.Verify(senderStellarToml.SigningKey, []byte(authreq.Data), signatureBytes) if err != nil { log.WithFields(log.Fields{ "signing_key": senderStellarToml.SigningKey, "data": authreq.Data, "sig": authreq.Signature, }).Warn("Invalid signature") errorResponse := protocols.NewInvalidParameterError("sig", authreq.Signature) server.Write(w, errorResponse) return } b64r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(authData.Tx)) var tx xdr.Transaction _, err = xdr.Unmarshal(b64r, &tx) if err != nil { errorResponse := protocols.NewInvalidParameterError("data.tx", authData.Tx) log.WithFields(log.Fields{ "err": err, "tx": authData.Tx, }).Warn("Error decoding Transaction XDR") server.Write(w, errorResponse) return } if tx.Memo.Hash == nil { errorResponse := protocols.NewInvalidParameterError("data.tx", authData.Tx) log.WithFields(log.Fields{"tx": authData.Tx}).Warn("Transaction does not contain Memo.Hash") server.Write(w, errorResponse) return } // Validate memo preimage hash memoPreimageHashBytes := sha256.Sum256([]byte(authData.Memo)) memoBytes := [32]byte(*tx.Memo.Hash) if memoPreimageHashBytes != memoBytes { errorResponse := protocols.NewInvalidParameterError("data.tx", authData.Tx) h := xdr.Hash(memoPreimageHashBytes) tx.Memo.Hash = &h var txBytes bytes.Buffer _, err = xdr.Marshal(&txBytes, tx) if err != nil { log.Error("Error mashaling transaction") server.Write(w, protocols.InternalServerError) return } expectedTx := base64.StdEncoding.EncodeToString(txBytes.Bytes()) log.WithFields(log.Fields{"tx": authData.Tx, "expected_tx": expectedTx}).Warn("Memo preimage hash does not equal tx Memo.Hash") server.Write(w, errorResponse) return } var memoPreimage memo.Memo err = json.Unmarshal([]byte(authData.Memo), &memoPreimage) if err != nil { errorResponse := protocols.NewInvalidParameterError("data.memo", authData.Memo) log.WithFields(log.Fields{ "err": err, "memo": authData.Memo, }).Warn("Cannot unmarshal memo preimage") server.Write(w, errorResponse) return } transactionHash, err := submitter.TransactionHash(&tx, rh.Config.NetworkPassphrase) if err != nil { log.WithFields(log.Fields{"err": err}).Warn("Error calculating tx hash") server.Write(w, protocols.InternalServerError) return } response := compliance.AuthResponse{} // Sanctions check if rh.Config.Callbacks.Sanctions == "" { response.TxStatus = compliance.AuthStatusOk } else { resp, err := rh.Client.PostForm( rh.Config.Callbacks.Sanctions, url.Values{"sender": {memoPreimage.Transaction.SenderInfo}}, ) if err != nil { log.WithFields(log.Fields{ "sanctions": rh.Config.Callbacks.Sanctions, "err": err, }).Error("Error sending request to sanctions server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Error reading sanctions server response") server.Write(w, protocols.InternalServerError) return } switch resp.StatusCode { case http.StatusOK: // AuthStatusOk response.TxStatus = compliance.AuthStatusOk case http.StatusAccepted: // AuthStatusPending response.TxStatus = compliance.AuthStatusPending var pendingResponse compliance.PendingResponse err := json.Unmarshal(body, &pendingResponse) if err != nil { // Set default value response.Pending = 600 } else { response.Pending = pendingResponse.Pending } case http.StatusForbidden: // AuthStatusDenied response.TxStatus = compliance.AuthStatusDenied default: log.WithFields(log.Fields{ "status": resp.StatusCode, "body": string(body), }).Error("Error response from sanctions server") server.Write(w, protocols.InternalServerError) return } } // User info if authData.NeedInfo { if rh.Config.Callbacks.AskUser == "" { response.InfoStatus = compliance.AuthStatusDenied // Check AllowedFi tokens := strings.Split(authData.Sender, "*") if len(tokens) != 2 { log.WithFields(log.Fields{ "sender": authData.Sender, }).Warn("Invalid stellar address") server.Write(w, protocols.InternalServerError) return } allowedFi, err := rh.Repository.GetAllowedFiByDomain(tokens[1]) if err != nil { log.WithFields(log.Fields{"err": err}).Error("Error getting AllowedFi from DB") server.Write(w, protocols.InternalServerError) return } if allowedFi == nil { // FI not found check AllowedUser allowedUser, err := rh.Repository.GetAllowedUserByDomainAndUserID(tokens[1], tokens[0]) if err != nil { log.WithFields(log.Fields{"err": err}).Error("Error getting AllowedUser from DB") server.Write(w, protocols.InternalServerError) return } if allowedUser != nil { response.InfoStatus = compliance.AuthStatusOk } } else { response.InfoStatus = compliance.AuthStatusOk } } else { // Ask user var amount, assetType, assetCode, assetIssuer string if len(tx.Operations) > 0 { operationBody := tx.Operations[0].Body if operationBody.Type == xdr.OperationTypePayment { amount = baseAmount.String(operationBody.PaymentOp.Amount) operationBody.PaymentOp.Asset.Extract(&assetType, &assetCode, &assetIssuer) } else if operationBody.Type == xdr.OperationTypePathPayment { amount = baseAmount.String(operationBody.PathPaymentOp.DestAmount) operationBody.PathPaymentOp.DestAsset.Extract(&assetType, &assetCode, &assetIssuer) } } resp, err := rh.Client.PostForm( rh.Config.Callbacks.AskUser, url.Values{ "amount": {amount}, "asset_code": {assetCode}, "asset_issuer": {assetIssuer}, "sender": {memoPreimage.Transaction.SenderInfo}, "note": {memoPreimage.Transaction.Note}, }, ) if err != nil { log.WithFields(log.Fields{ "ask_user": rh.Config.Callbacks.AskUser, "err": err, }).Error("Error sending request to ask_user server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Error("Error reading ask_user server response") server.Write(w, protocols.InternalServerError) return } switch resp.StatusCode { case http.StatusOK: // AuthStatusOk response.InfoStatus = compliance.AuthStatusOk case http.StatusAccepted: // AuthStatusPending response.InfoStatus = compliance.AuthStatusPending var pendingResponse compliance.PendingResponse err := json.Unmarshal(body, &pendingResponse) if err != nil { // Set default value response.Pending = 600 } else { response.Pending = pendingResponse.Pending } case http.StatusForbidden: // AuthStatusDenied response.InfoStatus = compliance.AuthStatusDenied default: log.WithFields(log.Fields{ "status": resp.StatusCode, "body": string(body), }).Error("Error response from ask_user server") server.Write(w, protocols.InternalServerError) return } } if response.InfoStatus == compliance.AuthStatusOk { // Fetch Info fetchInfoRequest := compliance.FetchInfoRequest{Address: memoPreimage.Transaction.Route} resp, err := rh.Client.PostForm( rh.Config.Callbacks.FetchInfo, fetchInfoRequest.ToValues(), ) if err != nil { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "err": err, }).Error("Error sending request to fetch_info server") server.Write(w, protocols.InternalServerError) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "err": err, }).Error("Error reading fetch_info server response") server.Write(w, protocols.InternalServerError) return } if resp.StatusCode != http.StatusOK { log.WithFields(log.Fields{ "fetch_info": rh.Config.Callbacks.FetchInfo, "status": resp.StatusCode, "body": string(body), }).Error("Error response from fetch_info server") server.Write(w, protocols.InternalServerError) return } response.DestInfo = string(body) } } else { response.InfoStatus = compliance.AuthStatusOk } if response.TxStatus == compliance.AuthStatusOk && response.InfoStatus == compliance.AuthStatusOk { authorizedTransaction := &entities.AuthorizedTransaction{ TransactionID: hex.EncodeToString(transactionHash[:]), Memo: base64.StdEncoding.EncodeToString(memoBytes[:]), TransactionXdr: authData.Tx, AuthorizedAt: time.Now(), Data: authreq.Data, } err = rh.EntityManager.Persist(authorizedTransaction) if err != nil { log.WithFields(log.Fields{"err": err}).Warn("Error persisting AuthorizedTransaction") server.Write(w, protocols.InternalServerError) return } } server.Write(w, &response) }