#!/usr/bin/env python3
# Autor programu: PB <pavel.bartos.pb@gmail.com>
"""Example check-in sender. Python 3 standard library. Does not install updates."""
import argparse,datetime,json,os,pathlib,re,secrets,socket,sys,urllib.error,urllib.parse,urllib.request,uuid

def main():
 parser=argparse.ArgumentParser(description=__doc__)
 parser.add_argument('--software-id',required=True,help='Stable project software_id, e.g. eweb')
 parser.add_argument('--software',help='Program name; defaults to software-id')
 parser.add_argument('--version',required=True,help='Actually installed version')
 parser.add_argument('--hostname',default=socket.getfqdn(),help='Actual server hostname/FQDN')
 parser.add_argument('--installed-at',help='Actual installation date YYYY-MM-DD; omit if unknown')
 parser.add_argument('--endpoint',default='https://update.estudium.eu/check/')
 parser.add_argument('--state-dir',help='Private directory unique to this program installation')
 args=parser.parse_args()
 if not re.fullmatch(r'[a-z0-9][a-z0-9-]{1,79}',args.software_id):parser.error('Invalid software-id')
 if not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}',args.version):parser.error('Invalid version')
 endpoint=urllib.parse.urlsplit(args.endpoint)
 if endpoint.scheme!='https' or not endpoint.hostname or endpoint.username or endpoint.password or endpoint.fragment:parser.error('Endpoint must be HTTPS without credentials or a fragment')
 if args.installed_at:
  if not re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}',args.installed_at):parser.error('installed-at must use YYYY-MM-DD')
  try:datetime.date.fromisoformat(args.installed_at)
  except ValueError:parser.error('installed-at must be an actual YYYY-MM-DD date')
 directory=pathlib.Path(args.state_dir).expanduser() if args.state_dir else pathlib.Path.home()/'.local/state/eupdate-checkin'/args.software_id
 directory.mkdir(mode=0o700,parents=True,exist_ok=True)
 if os.name=='posix' and directory.stat().st_mode&0o077:parser.error('State directory must be private (mode 700)')
 statefile=directory/'identity.json'
 try:
  # Exclusive creation prevents two first runs from issuing different identities.
  fd=os.open(statefile,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
 except FileExistsError:
  if statefile.is_symlink():parser.error('Identity file must not be a symbolic link')
  if os.name=='posix' and statefile.stat().st_mode&0o077:parser.error('Identity file must be private (mode 600)')
  state=json.loads(statefile.read_text(encoding='utf-8'))
 else:
  state={'author':'PB <pavel.bartos.pb@gmail.com>','software_id':args.software_id,'installation_id':str(uuid.uuid4()),'api_key':secrets.token_hex(32)}
  with os.fdopen(fd,'w',encoding='utf-8') as output:json.dump(state,output,indent=2);output.flush();os.fsync(output.fileno())
 if state.get('software_id')!=args.software_id or not re.fullmatch(r'[a-f0-9]{64}',state.get('api_key','')):parser.error('Invalid identity or state directory belongs to another program')
 installation={'id':state['installation_id']}
 if args.installed_at:installation['installed_at']=args.installed_at
 payload={'schema':'eupdate-checkin-v1','server_id':state['installation_id'],'checkin_id':uuid.uuid4().hex,'software':args.software or args.software_id,'software_id':args.software_id,'version':args.version,'installation':installation,'system':{'hostname':args.hostname}}
 request=urllib.request.Request(args.endpoint,data=json.dumps(payload).encode('utf-8'),headers={'Content-Type':'application/json','Accept':'application/json','X-Checkin-Api-Key':state['api_key'],'User-Agent':'eUpdate-checkin-example/1.1.0'},method='POST')
 # Do not redirect an authenticated request and its key to a different origin.
 class NoRedirect(urllib.request.HTTPRedirectHandler):
  def redirect_request(self,*args,**kwargs):return None
 try:
  with urllib.request.build_opener(NoRedirect).open(request,timeout=30) as response:
   result=json.loads(response.read(1048576).decode('utf-8'))
  if not result.get('ok'):raise ValueError('Checker did not accept the report')
 except urllib.error.HTTPError as error:
  print('Check-in rejected: HTTP '+str(error.code)+'. Check the endpoint or ask the administrator to inspect the checker log.',file=sys.stderr);return 1
 except (urllib.error.URLError,ValueError,OSError) as error:
  print('Check-in failed: '+str(error),file=sys.stderr);return 1
 print(json.dumps(result,ensure_ascii=False,indent=2))
 return 0
if __name__=='__main__':sys.exit(main())
