api-terminals.go (13297B)
1 // This file is part of taler-cashless2ecash. 2 // Copyright (C) 2024 Joel Häberli 3 // 4 // taler-cashless2ecash is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, 7 // or (at your option) any later version. 8 // 9 // taler-cashless2ecash is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // SPDX-License-Identifier: AGPL3.0-or-later 18 19 package internal_api 20 21 import ( 22 internal_utils "c2ec/internal/utils" 23 "c2ec/pkg/config" 24 "c2ec/pkg/db" 25 "crypto/rand" 26 "errors" 27 "fmt" 28 "net/http" 29 ) 30 31 type TerminalConfig struct { 32 Name string `json:"name"` 33 Version string `json:"version"` 34 ProviderName string `json:"provider_name"` 35 Currency string `json:"currency"` 36 WithdrawalFees string `json:"withdrawal_fees"` 37 WireType string `json:"wire_type"` 38 } 39 40 type TerminalWithdrawalSetup struct { 41 Amount string `json:"amount"` 42 SuggestedAmount string `json:"suggested_amount"` 43 ProviderTransactionId string `json:"provider_transaction_id"` 44 TerminalFees string `json:"terminal_fees"` 45 RequestUid string `json:"request_uid"` 46 UserUuid string `json:"user_uuid"` 47 Lock string `json:"lock"` 48 } 49 50 type TerminalWithdrawalSetupResponse struct { 51 Wopid string `json:"withdrawal_id"` 52 } 53 54 type TerminalWithdrawalConfirmationRequest struct { 55 ProviderTransactionId string `json:"provider_transaction_id"` 56 TerminalFees string `json:"terminal_fees"` 57 UserUuid string `json:"user_uuid"` 58 Lock string `json:"lock"` 59 } 60 61 func HandleTerminalConfig(res http.ResponseWriter, req *http.Request) { 62 63 p, auth, err := authAndParseProvider(req) 64 if !auth { 65 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED) 66 res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED) 67 return 68 } 69 70 if err != nil || p == nil { 71 internal_utils.LogError("terminals-api", err) 72 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 73 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 74 return 75 } 76 77 encoder := internal_utils.NewJsonCodec[TerminalConfig]() 78 cfg, err := encoder.EncodeToBytes(&TerminalConfig{ 79 Name: "taler-terminal", 80 Version: "0:0:0", 81 ProviderName: p.Name, 82 Currency: config.CONFIG.Server.Currency, 83 WithdrawalFees: config.CONFIG.Server.WithdrawalFees, 84 WireType: p.PaytoTargetType, 85 }) 86 if err != nil { 87 internal_utils.LogError("terminals-api", err) 88 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 89 res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 90 return 91 } 92 93 res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, encoder.HttpApplicationContentHeader()) 94 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_OK) 95 res.WriteHeader(internal_utils.HTTP_OK) 96 res.Write(cfg) 97 } 98 99 func HandleWithdrawalSetup(res http.ResponseWriter, req *http.Request) { 100 101 p, auth, err := authAndParseProvider(req) 102 if !auth { 103 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED) 104 res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED) 105 return 106 } 107 if err != nil || p == nil { 108 internal_utils.LogError("terminals-api", err) 109 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 110 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 111 return 112 } 113 114 jsonCodec := internal_utils.NewJsonCodec[TerminalWithdrawalSetup]() 115 setup, err := internal_utils.ReadStructFromBody[TerminalWithdrawalSetup](req, jsonCodec) 116 if err != nil { 117 internal_utils.LogWarn("terminals-api", fmt.Sprintf("invalid body for withdrawal registration error=%s", err.Error())) 118 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 119 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 120 return 121 } 122 123 if hasConflict(setup) { 124 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_CONFLICT) 125 res.WriteHeader(internal_utils.HTTP_CONFLICT) 126 return 127 } 128 129 terminalId := parseTerminalId(req) 130 if terminalId == -1 { 131 internal_utils.LogWarn("terminals-api", "terminal id could not be read from authorization header") 132 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 133 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 134 return 135 } 136 137 // generate wopid 138 generatedWopid := make([]byte, 32) 139 _, err = rand.Read(generatedWopid) 140 if err != nil { 141 internal_utils.LogWarn("terminals-api", "unable to generate correct wopid") 142 internal_utils.LogError("terminals-api", err) 143 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 144 res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 145 } 146 147 suggstdAmnt, err := parseAmount(setup.SuggestedAmount) 148 if err != nil { 149 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 150 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 151 return 152 } 153 amnt, err := parseAmount(setup.Amount) 154 if err != nil { 155 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 156 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 157 return 158 } 159 fees, err := parseAmount(setup.TerminalFees) 160 if err != nil { 161 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 162 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 163 return 164 } 165 166 err = db.DB.SetupWithdrawal( 167 generatedWopid, 168 suggstdAmnt, 169 amnt, 170 terminalId, 171 setup.ProviderTransactionId, 172 fees, 173 setup.RequestUid, 174 ) 175 176 if err != nil { 177 internal_utils.LogError("terminals-api", err) 178 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 179 res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 180 return 181 } 182 183 encoder := internal_utils.NewJsonCodec[TerminalWithdrawalSetupResponse]() 184 encodedBody, err := encoder.EncodeToBytes( 185 &TerminalWithdrawalSetupResponse{ 186 Wopid: internal_utils.TalerBinaryEncode(generatedWopid), 187 }, 188 ) 189 if err != nil { 190 internal_utils.LogError("terminal-api", err) 191 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 192 res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 193 return 194 } 195 196 res.Header().Add(internal_utils.CONTENT_TYPE_HEADER, encoder.HttpApplicationContentHeader()) 197 res.Write(encodedBody) 198 } 199 200 func HandleWithdrawalCheck(res http.ResponseWriter, req *http.Request) { 201 202 p, auth, err := authAndParseProvider(req) 203 if !auth { 204 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED) 205 res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED) 206 return 207 } 208 209 if err != nil || p == nil { 210 internal_utils.LogError("terminals-api", err) 211 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 212 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 213 return 214 } 215 216 wopid := req.PathValue(WOPID_PARAMETER) 217 wpd, err := internal_utils.ParseWopid(wopid) 218 if err != nil { 219 internal_utils.LogWarn("terminals-api", "wopid "+wopid+" not valid") 220 if wopid == "" { 221 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 222 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 223 return 224 } 225 } 226 227 jsonCodec := internal_utils.NewJsonCodec[TerminalWithdrawalConfirmationRequest]() 228 paymentNotification, err := internal_utils.ReadStructFromBody[TerminalWithdrawalConfirmationRequest](req, jsonCodec) 229 if err != nil { 230 internal_utils.LogError("terminals-api", err) 231 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 232 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 233 return 234 } 235 236 internal_utils.LogInfo("terminals-api", "received payment notification") 237 238 terminalId := parseTerminalId(req) 239 if terminalId == -1 { 240 internal_utils.LogWarn("terminals-api", "terminal id could not be read from authorization header") 241 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 242 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 243 return 244 } 245 246 trmlFees, err := internal_utils.ParseAmount(paymentNotification.TerminalFees, config.CONFIG.Server.CurrencyFractionDigits) 247 if err != nil { 248 internal_utils.LogError("terminals-api", err) 249 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 250 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 251 return 252 } 253 254 exchangeFees, err := parseAmount(config.CONFIG.Server.WithdrawalFees) 255 if err != nil { 256 internal_utils.LogError("terminals-api", errors.New("unable to parse withdrawal fees - FATAL SHOULD NEVER HAPPEN")) 257 internal_utils.LogError("terminals-api", err) 258 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 259 res.WriteHeader(internal_utils.HTTP_INTERNAL_SERVER_ERROR) 260 return 261 } 262 263 // Fees are optional here and since the Exchange can specify 264 // zero fees, the value can be zero as well. The case that the 265 // the terminal sends no fees and the exchange does not charge 266 // fees needs to be covered as compliant request, currently done 267 // by the trmlFees < exchangeFees check. 268 // Check that fees are at least as high as the configured withdrawal fees. 269 // a higher value would indicate that the payment service provider does 270 // also charge fees. 271 // incoming fees >= specified fees 272 if smaller, err := trmlFees.IsSmallerThan(exchangeFees); smaller || err != nil { 273 if err != nil { 274 internal_utils.LogError("terminals-api", err) 275 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 276 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 277 return 278 } 279 if smaller { 280 internal_utils.LogError("terminals-api", errors.New("terminal did specify uncorrect fees")) 281 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 282 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 283 return 284 } 285 } 286 287 internal_utils.LogInfo("terminals-api", "received valid check request for provider_transaction_id="+paymentNotification.ProviderTransactionId) 288 err = db.DB.NotifyPayment( 289 wpd, 290 paymentNotification.ProviderTransactionId, 291 terminalId, 292 preventNilAmount(trmlFees), 293 ) 294 if err != nil { 295 internal_utils.LogError("terminals-api", err) 296 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_BAD_REQUEST) 297 res.WriteHeader(internal_utils.HTTP_BAD_REQUEST) 298 return 299 } 300 301 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_NO_CONTENT) 302 res.WriteHeader(internal_utils.HTTP_NO_CONTENT) 303 } 304 305 func HandleWithdrawalStatusTerminal(res http.ResponseWriter, req *http.Request) { 306 307 _, auth, err := authAndParseProvider(req) 308 if err != nil || !auth { 309 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED) 310 res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED) 311 return 312 } 313 314 HandleWithdrawalStatus(res, req) 315 } 316 317 func HandleWithdrawalAbortTerminal(res http.ResponseWriter, req *http.Request) { 318 319 _, auth, err := authAndParseProvider(req) 320 if err != nil || !auth { 321 internal_utils.SetLastResponseCodeForLogger(internal_utils.HTTP_UNAUTHORIZED) 322 res.WriteHeader(internal_utils.HTTP_UNAUTHORIZED) 323 return 324 } 325 326 HandleWithdrawalAbort(res, req) 327 } 328 329 func parseAmount(amountStr string) (internal_utils.Amount, error) { 330 331 a, err := internal_utils.ParseAmount(amountStr, config.CONFIG.Server.CurrencyFractionDigits) 332 if err != nil { 333 return internal_utils.Amount{Currency: "", Value: 0, Fraction: 0}, err 334 } 335 return preventNilAmount(a), nil 336 } 337 338 func preventNilAmount(exchangeFees *internal_utils.Amount) internal_utils.Amount { 339 340 if exchangeFees == nil { 341 return internal_utils.Amount{Currency: "", Value: 0, Fraction: 0} 342 } 343 344 return *exchangeFees 345 } 346 347 func hasConflict(t *TerminalWithdrawalSetup) bool { 348 349 w, err := db.DB.GetWithdrawalByRequestUid(t.RequestUid) 350 if err != nil { 351 internal_utils.LogError("terminals-api", err) 352 return true 353 } 354 355 if w == nil { 356 return false // no request with this uid 357 } 358 359 suggstdAmnt, err := parseAmount(t.SuggestedAmount) 360 if err != nil { 361 internal_utils.LogError("terminals-api", err) 362 return true 363 } 364 amnt, err := parseAmount(t.Amount) 365 if err != nil { 366 internal_utils.LogError("terminals-api", err) 367 return true 368 } 369 fees, err := parseAmount(t.TerminalFees) 370 if err != nil { 371 internal_utils.LogError("terminals-api", err) 372 return true 373 } 374 375 isEqual := w.Amount.Curr == amnt.Currency && 376 w.Amount.Val == int64(amnt.Value) && 377 w.Amount.Frac == int32(amnt.Fraction) && 378 w.TerminalFees.Curr == fees.Currency && 379 uint64(w.TerminalFees.Val) == fees.Value && 380 uint64(w.TerminalFees.Frac) == fees.Fraction && 381 w.SuggestedAmount.Curr == suggstdAmnt.Currency && 382 uint64(w.SuggestedAmount.Val) == suggstdAmnt.Value && 383 uint64(w.SuggestedAmount.Frac) == suggstdAmnt.Fraction && 384 w.ProviderTransactionId == &t.ProviderTransactionId && 385 w.RequestUid == t.RequestUid 386 387 return !isEqual 388 } 389 390 func authAndParseProvider(req *http.Request) (*db.Provider, bool, error) { 391 392 if authenticated := AuthenticateTerminal(req); !authenticated { 393 return nil, false, nil 394 } 395 396 p, err := parseProvider(req) 397 if err != nil { 398 return nil, true, err 399 } 400 401 return p, true, nil 402 }