Blame proxyplugin.py

7ef9df
7ef9df
from __future__ import unicode_literals, absolute_import
7ef9df
7ef9df
import base64
7ef9df
import urllib.parse
7ae3bf
import os
7ae3bf
import tempfile
7ae3bf
import subprocess
7ae3bf
import shutil
7ef9df
from http.client import HTTPConnection
7ef9df
7ef9df
import flask
7ef9df
from werkzeug.datastructures import Headers
7ef9df
7ef9df
import pagure.exceptions
7ef9df
import pagure.lib.query
7ef9df
import pagure.lib.login
7ef9df
import pagure.utils
7ef9df
7ef9df
7ef9df
print("proxy loaded")
7ef9df
7ef9df
PROXYPLUGIN = flask.Blueprint("proxyplugin", __name__)
7ef9df
HOST = None
7ef9df
PORT = None
7ef9df
7ef9df
7ae3bf
def convert_ssl_to_ssh(sslcert):
7ae3bf
  """ Extract RSA key from certificate and represent in in ssh-rsa format
7ae3bf
  """
7ae3bf
  sslcert = str(sslcert).strip()
7ae3bf
  if not sslcert:
7ae3bf
    return None
7ae3bf
  tmpdirname = tempfile.mkdtemp()
7ae3bf
7ae3bf
  sslcert_filename = os.path.join(tmpdirname, "ssl.crt")
7ae3bf
  rsapem_filename = os.path.join(tmpdirname, "public-rsa.pem")
7ae3bf
  with open(sslcert_filename, "w") as stream:
7ae3bf
    stream.write(sslcert)
7ae3bf
7ae3bf
  # extract PEM RSA public key
7ae3bf
  cmd = ["/usr/bin/openssl", "x509", "-in", sslcert_filename, "-pubkey", "-noout", "-out", rsapem_filename]
7ae3bf
  proc = subprocess.Popen(
7ae3bf
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
7ae3bf
  stdout, stderr = proc.communicate()
7ae3bf
  if proc.returncode != 0:
7ae3bf
    print("openssl STDOUT: %s", stdout)
7ae3bf
    print("openssl STDERR: %s", stderr)
7ae3bf
    shutil.rmtree(tmpdirname)
7ae3bf
    return None
7ae3bf
7ae3bf
  # make ssh-ras from PEM RSA public key
7ae3bf
  cmd = ["/usr/bin/ssh-keygen", "-f", rsapem_filename, "-i", "-mPKCS8"]
7ae3bf
  proc = subprocess.Popen(
7ae3bf
    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
7ae3bf
  stdout, stderr = proc.communicate()
7ae3bf
  if proc.returncode != 0:
7ae3bf
    print("ssh-keygen STDOUT: %s", stdout)
7ae3bf
    print("ssh-keygen STDERR: %s", stderr)
7ae3bf
    shutil.rmtree(tmpdirname)
7ae3bf
    return None
7ae3bf
7ae3bf
  shutil.rmtree(tmpdirname)
7ae3bf
  sshrsa = stdout.decode("utf-8")
7ae3bf
7ae3bf
  return sshrsa
7ae3bf
7ae3bf
7ef9df
@PROXYPLUGIN.route("/repos/<path:_>", methods=["GET", "POST"])</path:_>
7ef9df
def repos(_):
7ef9df
  """ Proxy to git http backend.
7ef9df
  """
7ef9df
  
7ef9df
  print("repos called")
7ef9df
7ef9df
  env = flask.request.environ
7ef9df
  session = flask.g.session
7ef9df
  if hasattr(flask.g, "fas_user"):
7ef9df
    print("user logged in by cookie")
7ef9df
    return forbidden() # disallow cookie authorization
7ef9df
7ef9df
  # read login and password
7ef9df
  login = None
7ef9df
  password = None
7ef9df
  if 'HTTP_AUTHORIZATION' in env:
7ef9df
    try:
7ef9df
      authtype, credentials = str(env['HTTP_AUTHORIZATION']).split()
7ef9df
      assert authtype.lower() == 'basic'
7ef9df
      login, password = base64.b64decode(credentials).decode('utf8').split(':')
7ef9df
    except Exception:
7ef9df
      print("wrong auth format")
7ef9df
      return unauthorized()
7ae3bf
  elif 'PAGURE_SSL_CLIENT_CERT' in env:
7ae3bf
    sslcert = str(urllib.parse.unquote(env['PAGURE_SSL_CLIENT_CERT']))
7ae3bf
7ef9df
  # verify password
7ef9df
  if login:
7ef9df
    user_obj = pagure.lib.query.search_user(session, username = login)
7ef9df
    if not user_obj or user_obj.token:
7ef9df
      print("wrong user")
7ef9df
      return unauthorized()
7ef9df
    success = False
7ef9df
    try:
7ef9df
      success = pagure.lib.login.check_password(
7ef9df
        password,
7ef9df
        user_obj.password,
7ef9df
        seed = pagure.config.config.get("PASSWORD_SEED", None) )
7ef9df
    except pagure.exceptions.PagureException:
7ef9df
      pass
7ef9df
    if not success:
7ef9df
      print("wrong password")
7ef9df
      return unauthorized()
7ae3bf
  elif sslcert:
7ae3bf
    #print("received sslcert:", sslcert)
7ae3bf
    ssh_rsa = convert_ssl_to_ssh(sslcert)
7ae3bf
    if ssh_rsa:
7ae3bf
      #print("converted to ssh-rsa:", ssh_rsa)
7ae3bf
      ssh_short_key = pagure.lib.query.is_valid_ssh_key(ssh_rsa)
7ae3bf
      if ssh_short_key:
7ae3bf
        ssh_search_key = ssh_short_key.split(" ")[1]
7ae3bf
        #print("ssh-rsa hash:", ssh_search_key)
7ae3bf
        key = pagure.lib.query.find_ssh_key(session, ssh_search_key, None)
7ae3bf
        if key and key.user and not key.user.token:
7ae3bf
          login = key.user.username
7ae3bf
          print("login by sslcert:", login, ssh_search_key)
7ae3bf
7ef9df
  # read path
7ef9df
  path = env["REQUEST_URI"].split('?')[0].split('/')
7ef9df
  print(path)
7ef9df
  if len(path) < 3 or '.' in path or '..' in path:
7ef9df
    print("bad path")
7ef9df
    return forbidden()
7ef9df
  if path.pop(0) != '':
7ef9df
    print("no leading slash")
7ef9df
    return forbidden()
7ef9df
  if path.pop(0) != 'repos':
7ef9df
    print("not repos")
7ef9df
    return forbidden()
7ef9df
  
7ef9df
  # read reponame
7ef9df
  fork = False
7ae3bf
  docs = False
7ef9df
  repouser = None
7ef9df
  namespace = None
7ef9df
  reponame = path.pop(0)
7ae3bf
  if reponame == 'docs':
7ae3bf
    if len(path) < 1:
7ae3bf
      print("bad url format")
7ae3bf
      return forbidden()
7ae3bf
    docs = True
7ae3bf
    reponame = path.pop(0)
7ef9df
  if reponame == 'fork':
7ef9df
    if len(path) < 2:
7ef9df
      print("bad url format")
7ef9df
      return forbidden()
7ef9df
    fork = True
7ef9df
    repouser = path.pop(0)
7ef9df
    reponame = path.pop(0)
7ef9df
  if not reponame.endswith('.git'):
7ef9df
    if not path:
7ef9df
      print("bad url format")
7ef9df
      return forbidden()
7ef9df
    namespace = reponame
7ef9df
    reponame = path.pop(0)
7ef9df
  if not reponame.endswith('.git'):
7ef9df
    print("bad url format")
7ef9df
    return forbidden()
7ef9df
  reponame = reponame[:-4]
7ef9df
  
7ef9df
  # get repo
7ef9df
  repo = pagure.lib.query.get_authorized_project(
7ef9df
    session      = session,
7ef9df
    project_name = reponame,
7ef9df
    user         = repouser,
7ef9df
    namespace    = namespace,
7ef9df
    asuser       = login )
7ef9df
  if not repo:
7ef9df
    print("no repo [%s][%s][%s][%s]" % (str(fork), repouser, namespace, reponame))
7ef9df
    return forbidden() if login else unauthorized()
7ef9df
  
7ef9df
  # check access mode
7ef9df
  method = env["REQUEST_METHOD"]
7ef9df
  getvars = urllib.parse.parse_qs(env["QUERY_STRING"])
7ef9df
  readwrite = env["REQUEST_METHOD"] != 'GET'
7ef9df
  if method == 'GET' and getvars.get('service', list()).count('git-receive-pack'):
7ef9df
    readwrite = True 
7ef9df
  elif method == 'POST' and path and path[0] == 'git-upload-pack':
7ef9df
    readwrite = False
7ef9df
    
7ef9df
  # check write access
7ef9df
  if readwrite:
7ef9df
    print("write [%s]" % str(login))
7ef9df
  if readwrite and (not login or not pagure.utils.is_repo_committer(repo, login, session)):
7ef9df
    print("no write access")
7ef9df
    return forbidden() if login else unauthorized()
7ef9df
  
7ef9df
  print("pass to proxy")
7ef9df
  return httpproxy(HOST, PORT, login)
7ef9df
7ef9df
7ef9df
def badgateway():
7ef9df
  return flask.Response('502  Bad Gateway', status = '502  Bad Gateway')
7ef9df
def forbidden():
7ef9df
  return flask.Response('403 Forbidden', status = '403 Forbidden')
7ef9df
def unauthorized():
7ef9df
  return flask.Response(
7ef9df
    '401 Unauthorized',
7ef9df
    status = '401 Unauthorized',
7ef9df
    headers = Headers([('WWW-Authenticate', 'Basic ream="Authorization area", charset="UTF-8"')]) )
7ef9df
7ef9df
7ef9df
def reader(data, connection = None):
7ae3bf
  count = 0
7ef9df
  while True:
7ef9df
    chunk = data.read(4096)
7ef9df
    if not chunk: break
7ae3bf
    count += len(chunk)
7ef9df
    yield chunk
7ef9df
  if connection:
7ef9df
    connection.close()
7ae3bf
    print('reply done %d' % count)
7ae3bf
  else:
7ae3bf
    print('query done %d' % count)
7ef9df
7ef9df
7ef9df
def httpproxy(host, port, login):
7ef9df
  assert(host)
7ef9df
  assert(port)
7ef9df
  
7ef9df
  skip_headers = {
7ef9df
    'connection',
7ef9df
    'keep-alive',
7ef9df
    'proxy-authenticate',
7ef9df
    'proxy-authorization',
7ef9df
    'proxy-connection',
7ef9df
    'te',
7ef9df
    'trailers',
7ef9df
    'transfer-encoding',
7ef9df
    'upgrade',
7ae3bf
    'authorization',
7ae3bf
    'www-authenticate',
7ae3bf
    'gzip' }
7ef9df
7ef9df
  env = flask.request.environ
7ef9df
  
7ef9df
  # input method
7ef9df
  method = env["REQUEST_METHOD"]
7ae3bf
  print(method)
7ef9df
7ef9df
  # gather input headers
7ef9df
  header_prefix = 'HTTP_'
7ef9df
  proxy_headers = dict()
7ef9df
  for k, v in env.items():
7ef9df
    if k.startswith(header_prefix):
7ef9df
      kk = k[len(header_prefix):].lower().replace('_', '-')
7ef9df
      if not kk in skip_headers:
7ef9df
        proxy_headers[kk] = v
7ae3bf
  if 'CONTENT_TYPE' in env and env['CONTENT_TYPE']:
7ef9df
    proxy_headers['content-type'] = env['CONTENT_TYPE']
7ef9df
  try:
7ef9df
    length = int(env['CONTENT_LENGTH'])
7ef9df
    if length: proxy_headers['content-length'] = str(length)
7ef9df
  except Exception:
7ef9df
    pass
7ef9df
  proxy_headers['host'] = host
7ef9df
  if login:
7ef9df
    proxy_headers['pagure-user'] = login
7ae3bf
  #print(proxy_headers)
7ef9df
7ef9df
  # input uri
7ef9df
  uri = env["REQUEST_URI"]
7ae3bf
  print(uri)
7ef9df
7ef9df
  # input body
7ef9df
  body = reader(env['wsgi.input'])
7ef9df
7ef9df
  connection = None
7ef9df
  try:
7ef9df
    connection = HTTPConnection(host = host, port = port)
7ef9df
    connection.request(
7ef9df
      method,
7ef9df
      uri,
7ef9df
      body = body,
7ef9df
      headers = proxy_headers )
7ef9df
  except Exception as e:
7ef9df
    return badgateway()
7ef9df
7ef9df
  response = connection.getresponse()
7ef9df
  headers = list()
7ef9df
  for k, v in response.getheaders():
7ef9df
    if not k.lower() in skip_headers:
7ef9df
      headers.append( (k, v) )
7ef9df
7ef9df
  return flask.Response(
7ef9df
    reader(response, connection),
7ef9df
    str(response.status) + ' ' + str(response.reason),
7ef9df
    Headers(headers) )