On Sun, Jul 22, 2018 at 09:41:00PM +0200, Jakub Wilk wrote: > * Adrian Mariano <rad...@cox.net>, 2018-07-20, 19:49: > > I'm not sure about exactly the right way to validate the metals. I took > > the most relaxed route of just banning '!', > > Enumerating badness makes me nervous. It is generally considered a bad > security practice.
What do you mean by "enumerating badness"? > > How about whitelisting known-good metal names ("silver", "gold", > "platinum"), and ignoring everything else? That would be more-or-less how > currencies are currently handled. I could do it that way, I suppose. I was thinking about currency names in other languages, or the service offering other metals. Perhaps these things shouldn't be concerns. Version attached that whitelists metals.
#!/usr/bin/python # # units_cur for units, a program for updated currency exchange rates # # Copyright (C) 2017-2018 # Free Software Foundation, Inc # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # # This program was written by Adrian Mariano (adri...@gnu.org) # # For Python 2 & 3 compatibility from __future__ import absolute_import, division, print_function # # version = '4.3' # Version 4.3: 20 July 2018 # # Validate rate data from server # # Version 4.2: 18 April 2018 # # Handle case of empty/malformed entry returned from the server # # Version 4.1: 30 October 2017 # # Fixed to include USD in the list of currency codes. # # Version 4: 2 October 2017 # # Complete rewrite to use Yahoo YQL API due to removal of TimeGenie RSS feed. # Switched to requests library using JSON. One program now runs under # Python 2 or Python 3. Thanks to Ray Hamel for some help with this update. # Normal imports import requests import codecs from argparse import ArgumentParser from collections import OrderedDict from datetime import date from os import linesep from sys import exit, stderr, stdout outfile_name = 'currency.units' validmetals = ['gold','silver','platinum'] # valid metals from the server # This exchange rate table lists the currency ISO 4217 codes, their # long text names, and any fixed definitions. If the definition is # empty then units_cur will query the server for a value. currency = OrderedDict([ ('ATS', ['austriaschilling', '1|13.7603 euro']), ('BEF', ['belgiumfranc', '1|40.3399 euro']), ('CYP', ['cypruspound', '1|0.585274 euro']), ('EEK', ['estoniakroon', '1|15.6466 euro # Equal to 1|8 germanymark']), ('FIM', ['finlandmarkka', '1|5.94573 euro']), ('FRF', ['francefranc', '1|6.55957 euro']), ('DEM', ['germanymark', '1|1.95583 euro']), ('GRD', ['greecedrachma', '1|340.75 euro']), ('IEP', ['irelandpunt', '1|0.787564 euro']), ('ITL', ['italylira', '1|1936.27 euro']), ('LVL', ['latvialats', '1|0.702804 euro']), ('LTL', ['lithuanialitas', '1|3.4528 euro']), ('LUF', ['luxembourgfranc', '1|40.3399 euro']), ('MTL', ['maltalira', '1|0.4293 euro']), ('SKK', ['slovakiakornua', '1|30.1260 euro']), ('SIT', ['sloveniatolar', '1|239.640 euro']), ('ESP', ['spainpeseta', '1|166.386 euro']), ('NLG', ['netherlandsguilder','1|2.20371 euro']), ('PTE', ['portugalescudo', '1|200.482 euro']), ('CVE', ['capeverdeescudo', '1|110.265 euro']), ('BGN', ['bulgarialev', '1|1.9558 euro']), ('BAM', ['bosniaconvertiblemark','germanymark']), ('KMF', ['comorosfranc', '1|491.96775 euro']), ('XOF', ['westafricanfranc', '1|655.957 euro']), ('XPF', ['cfpfranc', '1|119.33 euro']), ('XAF', ['centralafricancfafranc','1|655.957 euro']), ('AED', ['uaedirham','']), ('AFN', ['afghanafghani','']), ('ALL', ['albanialek','']), ('AMD', ['armeniadram','']), ('AOA', ['angolakwanza','']), ('ARS', ['argentinapeso','']), ('AUD', ['australiadollar','']), ('AWG', ['arubaflorin','']), ('AZN', ['azerbaijanmanat','']), ('BAM', ['bosniaconvertiblemark','']), ('BBD', ['barbadosdollar','']), ('BDT', ['bangladeshtaka','']), ('BGN', ['bulgarialev','']), ('BHD', ['bahraindinar','']), ('BIF', ['burundifranc','']), ('BMD', ['bermudadollar','']), ('BND', ['bruneidollar','']), ('BOB', ['boliviaboliviano','']), ('BRL', ['brazilreal','']), ('BSD', ['bahamasdollar','']), ('BTN', ['bhutanngultrum','']), ('BWP', ['botswanapula','']), ('BYN', ['belarusruble','']), ('BYR', ['oldbelarusruble','10000 BYN']), ('BZD', ['belizedollar','']), ('CAD', ['canadadollar','']), ('CDF', ['drcfranccongolais','']), ('CHF', ['swissfranc','']), ('CLP', ['chilepeso','']), ('CNY', ['chinayuan','']), ('COP', ['colombiapeso','']), ('CRC', ['costaricacolon','']), ('CUP', ['cubapeso','']), ('CVE', ['capeverdeescudo','']), ('CZK', ['czechkoruna','']), ('DJF', ['djiboutifranc','']), ('DKK', ['denmarkkrona','']), ('DOP', ['dominicanrepublicpeso','']), ('DZD', ['algeriadinar','']), ('EGP', ['egyptpound','']), ('ERN', ['eritreanakfa','']), ('ETB', ['ethiopianbirr','']), ('EUR', ['euro','']), ('FJD', ['fijidollar','']), ('FKP', ['falklandislandspound','']), ('GBP', ['ukpound','']), ('GEL', ['georgialari','']), ('GHS', ['ghanacedi','']), ('GIP', ['gibraltarpound','']), ('GMD', ['gambiadalasi','']), ('GNF', ['guineafranc','']), ('GTQ', ['guatemalaquetzal','']), ('GYD', ['guyanadollar','']), ('HKD', ['hongkongdollar','']), ('HNL', ['honduraslempira','']), ('HRK', ['croatiakuna','']), ('HTG', ['haitigourde','']), ('HUF', ['hungariaforint','']), ('IDR', ['indonesiarupiah','']), ('ILS', ['israelnewshekel','']), ('INR', ['indiarupee','']), ('IQD', ['iraqdinar','']), ('IRR', ['iranrial','']), ('ISK', ['icelandkrona','']), ('JMD', ['jamaicadollar','']), ('JOD', ['jordandinar','']), ('JPY', ['japanyen','']), ('KES', ['kenyaschilling','']), ('KGS', ['kyrgyzstansom','']), ('KHR', ['cambodiariel','']), ('KMF', ['comorosfranc','']), ('KPW', ['northkoreawon','']), ('KRW', ['southkoreawon','']), ('KWD', ['kuwaitdinar','']), ('KYD', ['caymanislandsdollar','']), ('KZT', ['kazakhstantenge','']), ('LAK', ['laokip','']), ('LBP', ['lebanonpound','']), ('LKR', ['srilankanrupee','']), ('LRD', ['liberiadollar','']), ('LTL', ['lithuanialita','']), ('LVL', ['latvialat','']), ('LYD', ['libyadinar','']), ('MAD', ['moroccodirham','']), ('MDL', ['moldovaleu','']), ('MGA', ['madagascarariary','']), ('MKD', ['macedoniadenar','']), ('MMK', ['myanmarkyat','']), ('MNT', ['mongoliatugrik','']), ('MOP', ['macaupataca','']), ('MRO', ['mauritaniaouguiya','']), ('MUR', ['mauritiusrupee','']), ('MVR', ['maldiverufiyaa','']), ('MWK', ['malawikwacha','']), ('MXN', ['mexicopeso','']), ('MYR', ['malaysiaringgit','']), ('MZN', ['mozambicanmetical','']), ('NAD', ['namibiadollar','']), ('NGN', ['nigerianaira','']), ('NIO', ['nicaraguacordobaoro','']), ('NOK', ['norwaykrone','']), ('NPR', ['nepalrupee','']), ('NZD', ['newzealanddollar','']), ('OMR', ['omanrial','']), ('PAB', ['panamabalboa','']), ('PEN', ['perunuevosol','']), ('PGK', ['papuanewguineakina','']), ('PHP', ['philippinepeso','']), ('PKR', ['pakistanrupee','']), ('PLN', ['polandzloty','']), ('PYG', ['paraguayguarani','']), ('QAR', ['qatarrial','']), ('RON', ['romanianewlei','']), ('RSD', ['serbiadinar','']), ('RUB', ['russiaruble','']), ('RWF', ['rwandafranc','']), ('SAR', ['saudiarabiariyal','']), ('SBD', ['solomonislandsdollar','']), ('SCR', ['seychellesrupee','']), ('SDG', ['sudanpound','']), ('SEK', ['swedenkrona','']), ('SGD', ['singaporedollar','']), ('SHP', ['sainthelenapound','']), ('SLL', ['sierraleoneleone','']), ('SOS', ['somaliaschilling','']), ('SRD', ['surinamedollar','']), ('STD', ['saotome&principedobra','']), ('SVC', ['elsalvadorcolon','']), ('SYP', ['syriapound','']), ('SZL', ['swazilandlilangeni','']), ('THB', ['thailandbaht','']), ('TJS', ['tajikistansomoni','']), ('TMT', ['turkmenistanmanat','']), ('TND', ['tunisiadinar','']), ('TOP', ["tongapa'anga",'']), ('TRY', ['turkeylira','']), ('TTD', ['trinidadandtobagodollar','']), ('TWD', ['taiwandollar','']), ('TZS', ['tanzaniashilling','']), ('UAH', ['ukrainehryvnia','']), ('UGX', ['ugandaschilling','']), ('USD', ['unitedstatesdollar', 'US$']), ('UYU', ['uruguaypeso','']), ('UZS', ['uzbekistansum','']), ('VEF', ['venezuelabolivar','']), ('VEB', ['venezuelaoldbolivar', '1000 VEF']), ('VND', ['vietnamdong','']), ('VUV', ['vanuatuvatu','']), ('WST', ['samoatala','']), ('XAF', ['centralafricancfafranc','']), ('XCD', ['eastcaribbeandollar','']), ('XDR', ['specialdrawingrights','']), ('YER', ['yemenrial','']), ('ZAR', ['southafricarand','']), ('ZMW', ['zambiakwacha','']), ('ZWL', ['zimbabwedollar','']), ]) ap = ArgumentParser( description="Update currency information for 'units' " "into the specified filename or if no filename is " "given, the default: '{}'. The special filename '-' " "will send the currency data to stdout.".format(outfile_name), ) ap.add_argument( 'output_file', default=outfile_name, help='the file to update', metavar='filename', nargs='?', type=str, ) ap.add_argument('-V','--version', action='version', version='%(prog)s version ' + version, help='display units_cur version', ) ap.add_argument('-v','--verbose', action='store_true', help='display details when fetching currency data', ) def validfloat(x): try: float(x) return True except ValueError: return False outfile_name = ap.parse_args().output_file verbose = ap.parse_args().verbose try: res = requests.get('https://finance.yahoo.com/webservice/v1/symbols' '/allcurrencies/quote?format=json') res.raise_for_status() webdata = res.json()['list']['resources'] except requests.exceptions.RequestException as e: stderr.write('Error connecting to currency server:\n{}.\n'. format(e)) exit(1) rate_index = 1 for data in webdata: entry = data['resource']['fields'] if 'price' not in entry or 'symbol' not in entry: # Skip empty/bad entries if verbose: stderr.write('Got bad entry from server: '+str(entry)+'\n') else: rate = entry['price'] code = entry['symbol'][0:3] if code not in currency.keys(): if (verbose): stderr.write('Got unknown currency with code {}\n'.format(code)) else: if not currency[code][rate_index]: if validfloat(rate): currency[code][rate_index] = '1|{} US$'.format(rate) else: stderr.write('Got invalid rate "{}" for currency "{}"\n'.format( rate, code)) elif verbose: stderr.write('Got value "{}" for currency "{}" but ' 'it is already defined\n'.format(rate, code)) # Delete currencies where we have no rate data for code in currency.keys(): if not currency[code][rate_index]: if verbose: stderr.write('No data for {}'.format(code)) del currency[code] try: req = requests.get('https://services.packetizer.com/spotprices/?f=json') req.raise_for_status() metals = req.json() except requests.exceptions.RequestException as e: stderr.write('Error connecting to spotprices server:\n{}\n'.format(e)) exit(1) try: req = requests.get('https://services.packetizer.com/btc/?f=json') req.raise_for_status() bitcoin = req.json() except requests.exceptions.RequestException as e: stderr.write('Error connecting to bitcoin server:\n{}\n'.format(e)) exit(1) cnames = [currency[code][0] for code in currency.keys()] crates = [currency[code][1] for code in currency.keys()] codestr = '\n'.join('{:23}{}'. format(code, name) for (code,name) in zip(currency.keys(), cnames)) datestr = date.today().isoformat() maxlen = max(len(name) for name in cnames) + 2 ratestr = '\n'.join( '{:{}}{}'.format(name, maxlen, rate) for (name, rate) in zip(cnames, crates) ) metallist=[] for metal, price in metals.items(): if metal in validmetals: metallist.append('{:19}{} US$/troyounce'.format( metal + 'price', price)) elif metal != 'date': # Don't print a message for the "date" entry stderr.write('Got unknown metal "{}" with value "{}"\n',metal,price) metalstr = '\n'.join(metallist) if validfloat(bitcoin['usd']): bitcoinstr = '{:{}}{} US$ # From services.packetizer.com/btc\n'.format( 'bitcoin',maxlen,bitcoin['usd']) else: stderr.write('Got invalid bitcoin rate "{}"\n', bitcoint['usd']) bitcointstr='' outstr = ( """# ISO Currency Codes {codestr} # Currency exchange rates from Yahoo Finance (finance.yahoo.com) !message Currency exchange rates from finance.yahoo.com on {datestr} {ratestr} {bitcoinstr} # Precious metals prices from Packetizer (services.packetizer.com/spotprices) {metalstr} """.format(codestr=codestr, datestr=datestr, ratestr=ratestr, metalstr=metalstr, bitcoinstr=bitcoinstr) ).replace('\n', linesep) try: if outfile_name == '-': codecs.StreamReader(stdout, codecs.getreader('utf8')).write(outstr) else: with codecs.open(outfile_name, 'w', 'utf8') as of: of.write(outstr) except IOError as e: stderr.write('Unable to write to output file:\n{}\n'.format(e)) exit(1)