cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

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 }