Blob Blame Raw

from __future__ import unicode_literals, absolute_import

import base64
import urllib.parse
import os
import tempfile
import subprocess
import shutil
from http.client import HTTPConnection

import flask
from werkzeug.datastructures import Headers

import pagure.exceptions
import pagure.lib.query
import pagure.lib.login
import pagure.utils


print("proxy loaded")

PROXYPLUGIN = flask.Blueprint("proxyplugin", __name__)
HOST = None
PORT = None


def convert_ssl_to_ssh(sslcert):
  """ Extract RSA key from certificate and represent in in ssh-rsa format
  """
  sslcert = str(sslcert).strip()
  if not sslcert:
    return None
  tmpdirname = tempfile.mkdtemp()

  sslcert_filename = os.path.join(tmpdirname, "ssl.crt")
  rsapem_filename = os.path.join(tmpdirname, "public-rsa.pem")
  with open(sslcert_filename, "w") as stream:
    stream.write(sslcert)

  # extract PEM RSA public key
  cmd = ["/usr/bin/openssl", "x509", "-in", sslcert_filename, "-pubkey", "-noout", "-out", rsapem_filename]
  proc = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
  stdout, stderr = proc.communicate()
  if proc.returncode != 0:
    print("openssl STDOUT: %s", stdout)
    print("openssl STDERR: %s", stderr)
    shutil.rmtree(tmpdirname)
    return None

  # make ssh-ras from PEM RSA public key
  cmd = ["/usr/bin/ssh-keygen", "-f", rsapem_filename, "-i", "-mPKCS8"]
  proc = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
  stdout, stderr = proc.communicate()
  if proc.returncode != 0:
    print("ssh-keygen STDOUT: %s", stdout)
    print("ssh-keygen STDERR: %s", stderr)
    shutil.rmtree(tmpdirname)
    return None

  shutil.rmtree(tmpdirname)
  sshrsa = stdout.decode("utf-8")

  return sshrsa


@PROXYPLUGIN.route("/repos/<path:_>", methods=["GET", "POST"])
def repos(_):
  """ Proxy to git http backend.
  """
  
  print("repos called")

  env = flask.request.environ
  session = flask.g.session
  if hasattr(flask.g, "fas_user"):
    print("user logged in by cookie")
    return forbidden() # disallow cookie authorization

  # read login and password
  login = None
  password = None
  if 'HTTP_AUTHORIZATION' in env:
    try:
      authtype, credentials = str(env['HTTP_AUTHORIZATION']).split()
      assert authtype.lower() == 'basic'
      login, password = base64.b64decode(credentials).decode('utf8').split(':')
    except Exception:
      print("wrong auth format")
      return unauthorized()
  elif 'PAGURE_SSL_CLIENT_CERT' in env:
    sslcert = str(urllib.parse.unquote(env['PAGURE_SSL_CLIENT_CERT']))

  # verify password
  if login:
    user_obj = pagure.lib.query.search_user(session, username = login)
    if not user_obj or user_obj.token:
      print("wrong user")
      return unauthorized()
    success = False
    try:
      success = pagure.lib.login.check_password(
        password,
        user_obj.password,
        seed = pagure.config.config.get("PASSWORD_SEED", None) )
    except pagure.exceptions.PagureException:
      pass
    if not success:
      print("wrong password")
      return unauthorized()
  elif sslcert:
    #print("received sslcert:", sslcert)
    ssh_rsa = convert_ssl_to_ssh(sslcert)
    if ssh_rsa:
      #print("converted to ssh-rsa:", ssh_rsa)
      ssh_short_key = pagure.lib.query.is_valid_ssh_key(ssh_rsa)
      if ssh_short_key:
        ssh_search_key = ssh_short_key.split(" ")[1]
        #print("ssh-rsa hash:", ssh_search_key)
        key = pagure.lib.query.find_ssh_key(session, ssh_search_key, None)
        if key and key.user and not key.user.token:
          login = key.user.username
          print("login by sslcert:", login, ssh_search_key)

  # read path
  path = env["REQUEST_URI"].split('?')[0].split('/')
  print(path)
  if len(path) < 3 or '.' in path or '..' in path:
    print("bad path")
    return forbidden()
  if path.pop(0) != '':
    print("no leading slash")
    return forbidden()
  if path.pop(0) != 'repos':
    print("not repos")
    return forbidden()
  
  # read reponame
  fork = False
  docs = False
  repouser = None
  namespace = None
  reponame = path.pop(0)
  if reponame == 'docs':
    if len(path) < 1:
      print("bad url format")
      return forbidden()
    docs = True
    reponame = path.pop(0)
  if reponame == 'fork':
    if len(path) < 2:
      print("bad url format")
      return forbidden()
    fork = True
    repouser = path.pop(0)
    reponame = path.pop(0)
  if not reponame.endswith('.git'):
    if not path:
      print("bad url format")
      return forbidden()
    namespace = reponame
    reponame = path.pop(0)
  if not reponame.endswith('.git'):
    print("bad url format")
    return forbidden()
  reponame = reponame[:-4]
  
  # get repo
  repo = pagure.lib.query.get_authorized_project(
    session      = session,
    project_name = reponame,
    user         = repouser,
    namespace    = namespace,
    asuser       = login )
  if not repo:
    print("no repo [%s][%s][%s][%s]" % (str(fork), repouser, namespace, reponame))
    return forbidden() if login else unauthorized()
  
  # check access mode
  method = env["REQUEST_METHOD"]
  getvars = urllib.parse.parse_qs(env["QUERY_STRING"])
  readwrite = env["REQUEST_METHOD"] != 'GET'
  if method == 'GET' and getvars.get('service', list()).count('git-receive-pack'):
    readwrite = True 
  elif method == 'POST' and path and path[0] == 'git-upload-pack':
    readwrite = False
    
  # check write access
  if readwrite:
    print("write [%s]" % str(login))
  if readwrite and (not login or not pagure.utils.is_repo_committer(repo, login, session)):
    print("no write access")
    return forbidden() if login else unauthorized()
  
  print("pass to proxy")
  return httpproxy(HOST, PORT, login)


def badgateway():
  return flask.Response('502  Bad Gateway', status = '502  Bad Gateway')
def forbidden():
  return flask.Response('403 Forbidden', status = '403 Forbidden')
def unauthorized():
  return flask.Response(
    '401 Unauthorized',
    status = '401 Unauthorized',
    headers = Headers([('WWW-Authenticate', 'Basic ream="Authorization area", charset="UTF-8"')]) )


def reader(data, connection = None):
  count = 0
  while True:
    chunk = data.read(4096)
    if not chunk: break
    count += len(chunk)
    yield chunk
  if connection:
    connection.close()
    print('reply done %d' % count)
  else:
    print('query done %d' % count)


def httpproxy(host, port, login):
  assert(host)
  assert(port)
  
  skip_headers = {
    'connection',
    'keep-alive',
    'proxy-authenticate',
    'proxy-authorization',
    'proxy-connection',
    'te',
    'trailers',
    'transfer-encoding',
    'upgrade',
    'authorization',
    'www-authenticate',
    'gzip' }

  env = flask.request.environ
  
  # input method
  method = env["REQUEST_METHOD"]
  print(method)

  # gather input headers
  header_prefix = 'HTTP_'
  proxy_headers = dict()
  for k, v in env.items():
    if k.startswith(header_prefix):
      kk = k[len(header_prefix):].lower().replace('_', '-')
      if not kk in skip_headers:
        proxy_headers[kk] = v
  if 'CONTENT_TYPE' in env and env['CONTENT_TYPE']:
    proxy_headers['content-type'] = env['CONTENT_TYPE']
  try:
    length = int(env['CONTENT_LENGTH'])
    if length: proxy_headers['content-length'] = str(length)
  except Exception:
    pass
  proxy_headers['host'] = host
  if login:
    proxy_headers['pagure-user'] = login
  #print(proxy_headers)

  # input uri
  uri = env["REQUEST_URI"]
  print(uri)

  # input body
  body = reader(env['wsgi.input'])

  connection = None
  try:
    connection = HTTPConnection(host = host, port = port)
    connection.request(
      method,
      uri,
      body = body,
      headers = proxy_headers )
  except Exception as e:
    return badgateway()

  response = connection.getresponse()
  headers = list()
  for k, v in response.getheaders():
    if not k.lower() in skip_headers:
      headers.append( (k, v) )

  return flask.Response(
    reader(response, connection),
    str(response.status) + ' ' + str(response.reason),
    Headers(headers) )