Ejemplo n.º 1
0
def get_service_public_certificates(service_url):
  """Returns jsonish object with public certificates of a service.

  Service at |service_url| must have 'auth' component enabled (to serve
  the certificates).
  """
  cache_key = 'pub_certs:%s' % service_url
  certs = memcache.get(cache_key)
  if not certs:
    protocol = 'http://' if utils.is_local_dev_server() else 'https://'
    assert service_url.startswith(protocol)
    result = urlfetch.fetch(
        url='%s/auth/api/v1/server/certificates' % service_url,
        method='GET',
        headers={'X-URLFetch-Service-Id': utils.get_urlfetch_service_id()},
        follow_redirects=False,
        deadline=10,
        validate_certificate=True)
    if result.status_code != 200:
      raise CertificateError(
          'Failed to grab public certs from %s: HTTP %d' %
          (service_url, result.status_code))
    certs = json.loads(result.content)
    memcache.set(cache_key, certs, time=3600)
  return certs
Ejemplo n.º 2
0
def get_service_public_certificates(service_url):
    """Returns jsonish object with public certificates of a service.

  Service at |service_url| must have 'auth' component enabled (to serve
  the certificates).
  """
    cache_key = 'pub_certs:%s' % service_url
    certs = memcache.get(cache_key)
    if not certs:
        protocol = 'http://' if utils.is_local_dev_server() else 'https://'
        assert service_url.startswith(protocol)
        result = urlfetch.fetch(
            url='%s/auth/api/v1/server/certificates' % service_url,
            method='GET',
            headers={'X-URLFetch-Service-Id': utils.get_urlfetch_service_id()},
            follow_redirects=False,
            deadline=10,
            validate_certificate=True)
        if result.status_code != 200:
            raise CertificateError(
                'Failed to grab public certs from %s: HTTP %d' %
                (service_url, result.status_code))
        certs = json.loads(result.content)
        memcache.set(cache_key, certs, time=3600)
    return certs
Ejemplo n.º 3
0
def get_service_public_certificates(service_url):
    """Returns jsonish object with public certificates of a service.

  Service at |service_url| must have 'auth' component enabled (to serve
  the certificates).
  """
    cache_key = 'pub_certs:%s' % service_url
    certs = memcache.get(cache_key)
    if certs:
        return certs

    protocol = 'http://' if utils.is_local_dev_server() else 'https://'
    assert service_url.startswith(protocol)
    url = '%s/auth/api/v1/server/certificates' % service_url

    # Retry code is adapted from components/net.py. net.py can't be used directly
    # since it depends on components.auth (and dependency cycles between
    # components are bad).
    attempt = 0
    result = None
    while attempt < 4:
        if attempt:
            logging.info('Retrying...')
        attempt += 1
        logging.info('GET %s', url)
        try:
            result = urlfetch.fetch(url=url,
                                    method='GET',
                                    headers={
                                        'X-URLFetch-Service-Id':
                                        utils.get_urlfetch_service_id()
                                    },
                                    follow_redirects=False,
                                    deadline=2,
                                    validate_certificate=True)
        except (apiproxy_errors.DeadlineExceededError, urlfetch.Error) as e:
            # Transient network error or URL fetch service RPC deadline.
            logging.warning('GET %s failed: %s', url, e)
            continue
        # It MUST return 200 on success, it can't return 403, 404 or >=500.
        if result.status_code != 200:
            logging.warning('GET %s failed, HTTP %d: %r', url,
                            result.status_code, result.content)
            continue
        # Success.
        certs = json.loads(result.content)
        memcache.set(cache_key, certs, time=3600)
        return certs

    # All attempts failed, give up.
    msg = 'Failed to grab public certs from %s (HTTP code %s)' % (
        service_url, result.status_code if result else '???')
    raise CertificateError(msg, transient=True)
Ejemplo n.º 4
0
def get_service_public_certificates(service_url):
  """Returns jsonish object with public certificates of a service.

  Service at |service_url| must have 'auth' component enabled (to serve
  the certificates).
  """
  cache_key = 'pub_certs:%s' % service_url
  certs = memcache.get(cache_key)
  if certs:
    return certs

  protocol = 'http://' if utils.is_local_dev_server() else 'https://'
  assert service_url.startswith(protocol)
  url = '%s/auth/api/v1/server/certificates' % service_url

  # Retry code is adapted from components/net.py. net.py can't be used directly
  # since it depends on components.auth (and dependency cycles between
  # components are bad).
  attempt = 0
  result = None
  while attempt < 4:
    if attempt:
      logging.info('Retrying...')
    attempt += 1
    logging.info('GET %s', url)
    try:
      result = urlfetch.fetch(
          url=url,
          method='GET',
          headers={'X-URLFetch-Service-Id': utils.get_urlfetch_service_id()},
          follow_redirects=False,
          deadline=5,
          validate_certificate=True)
    except (apiproxy_errors.DeadlineExceededError, urlfetch.Error) as e:
      # Transient network error or URL fetch service RPC deadline.
      logging.warning('GET %s failed: %s', url, e)
      continue
    # It MUST return 200 on success, it can't return 403, 404 or >=500.
    if result.status_code != 200:
      logging.warning(
          'GET %s failed, HTTP %d: %r', url, result.status_code, result.content)
      continue
    # Success.
    certs = json.loads(result.content)
    memcache.set(cache_key, certs, time=3600)
    return certs

  # All attempts failed, give up.
  msg = 'Failed to grab public certs from %s (HTTP code %s)' % (
      service_url, result.status_code if result else '???')
  raise CertificateError(msg, transient=True)
Ejemplo n.º 5
0
def _fetch_service_certs(service_url):
    protocol = 'https://'
    if utils.is_local_dev_server():
        protocol = ('http://', 'https://')
    assert service_url.startswith(protocol), (service_url, protocol)
    url = '%s/auth/api/v1/server/certificates' % service_url

    # Retry code is adapted from components/net.py. net.py can't be used directly
    # since it depends on components.auth (and dependency cycles between
    # components are bad).
    attempt = 0
    result = None
    while attempt < 4:
        if attempt:
            logging.info('Retrying...')
        attempt += 1
        logging.info('GET %s', url)
        try:
            result = urlfetch.fetch(url=url,
                                    method='GET',
                                    headers={
                                        'X-URLFetch-Service-Id':
                                        utils.get_urlfetch_service_id()
                                    },
                                    follow_redirects=False,
                                    deadline=5,
                                    validate_certificate=True)
        except (apiproxy_errors.DeadlineExceededError, urlfetch.Error) as e:
            # Transient network error or URL fetch service RPC deadline.
            logging.warning('GET %s failed: %s', url, e)
            continue
        # It MUST return 200 on success, it can't return 403, 404 or >=500.
        if result.status_code != 200:
            logging.warning('GET %s failed, HTTP %d: %r', url,
                            result.status_code, result.content)
            continue
        return json.loads(result.content)

    # All attempts failed, give up.
    msg = 'Failed to grab public certs from %s (HTTP code %s)' % (
        service_url, result.status_code if result else '???')
    raise CertificateError(msg, transient=True)
Ejemplo n.º 6
0
def become_replica(ticket, initiated_by):
  """Converts current service to a replica of a primary specified in a ticket.

  Args:
    ticket: replication_pb2.ServiceLinkTicket passed from a primary.
    initiated_by: Identity of a user that accepted linking request, for logging.

  Raises:
    ProtocolError in case the request to primary fails.
  """
  assert model.is_standalone()

  # On dev appserver emulate X-Appengine-Inbound-Appid header.
  headers = {'Content-Type': 'application/octet-stream'}
  protocol = 'https'
  if utils.is_local_dev_server():
    headers['X-Appengine-Inbound-Appid'] = app_identity.get_application_id()
    protocol = 'http'
  headers['X-URLFetch-Service-Id'] = utils.get_urlfetch_service_id()

  # Pass back the ticket for primary to verify it, tell the primary to use
  # default version hostname to talk to us.
  link_request = replication_pb2.ServiceLinkRequest()
  link_request.ticket = ticket.ticket
  link_request.replica_url = (
      '%s://%s' % (protocol, app_identity.get_default_version_hostname()))
  link_request.initiated_by = initiated_by.to_bytes()

  # Primary will look at X-Appengine-Inbound-Appid and compare it to what's in
  # the ticket.
  try:
    result = urlfetch.fetch(
        url='%s/auth_service/api/v1/internal/link_replica' % ticket.primary_url,
        payload=link_request.SerializeToString(),
        method='POST',
        headers=headers,
        follow_redirects=False,
        deadline=30,
        validate_certificate=True)
  except urlfetch.Error as exc:
    raise ProtocolError(
        replication_pb2.ServiceLinkResponse.TRANSPORT_ERROR,
        'URLFetch error (%s): %s' % (exc.__class__.__name__, exc))

  # Protobuf based protocol is not using HTTP codes (handler always replies with
  # HTTP 200, providing error details if needed in protobuf serialized body).
  # So any other status code here means there was a transport level error.
  if result.status_code != 200:
    raise ProtocolError(
        replication_pb2.ServiceLinkResponse.TRANSPORT_ERROR,
        'Request to the primary failed with HTTP %d.' % result.status_code)

  link_response = replication_pb2.ServiceLinkResponse.FromString(result.content)
  if link_response.status != replication_pb2.ServiceLinkResponse.SUCCESS:
    message = LINKING_ERRORS.get(
        link_response.status,
        'Request to the primary failed with status %d.' % link_response.status)
    raise ProtocolError(link_response.status, message)

  # Become replica. Auth DB will be overwritten on a first push from Primary.
  state = model.AuthReplicationState(
      key=model.replication_state_key(),
      primary_id=ticket.primary_id,
      primary_url=ticket.primary_url)
  state.put()
Ejemplo n.º 7
0
def push_to_replica(replica_url, auth_db_blob, key_name, sig):
  """Pushes |auth_db_blob| to a replica via URLFetch POST.

  Args:
    replica_url: root URL of a replica (i.e. https://<host>).
    auth_db_blob: binary blob with serialized Auth DB.
    key_name: name of a RSA key used to generate a signature.
    sig: base64 encoded signature of |auth_db_blob|.

  Returns:
    Tuple:
      AuthDB revision reporter by a replica (as replication_pb2.AuthDBRevision).
      Auth component version used by replica (see components.auth.version).

  Raises:
    FatalReplicaUpdateError if replica rejected the push.
    TransientReplicaUpdateError if push should be retried.
  """
  replica_url = replica_url.rstrip('/')
  logging.info('Updating replica %s', replica_url)
  protocol = 'http://' if utils.is_local_dev_server() else 'https://'
  assert replica_url.startswith(protocol)

  # Pass signature via the headers.
  headers = {
    'Content-Type': 'application/octet-stream',
    'X-URLFetch-Service-Id': utils.get_urlfetch_service_id(),
    'X-AuthDB-SigKey-v1': key_name,
    'X-AuthDB-SigVal-v1': sig,
  }

  # On dev appserver emulate X-Appengine-Inbound-Appid header.
  if utils.is_local_dev_server():
    headers['X-Appengine-Inbound-Appid'] = app_identity.get_application_id()

  # 'follow_redirects' set to False is required for 'X-Appengine-Inbound-Appid'
  # to work. 70 sec deadline correspond to 60 sec GAE foreground requests
  # deadline plus 10 seconds to account for URL fetch own lags.
  ctx = ndb.get_context()
  result = yield ctx.urlfetch(
      url=replica_url + '/auth/api/v1/internal/replication',
      payload=auth_db_blob,
      method='POST',
      headers=headers,
      follow_redirects=False,
      deadline=70,
      validate_certificate=True)

  # Any transport level error is transient.
  if result.status_code != 200:
    raise TransientReplicaUpdateError(
        'Push request failed with HTTP code %d' % result.status_code)

  # Deserialize the response.
  cls = replication_pb2.ReplicationPushResponse
  response = cls.FromString(result.content)
  if not response.HasField('status'):
    raise FatalReplicaUpdateError('Incomplete response, status is missing')

  # Convert errors to exceptions.
  if response.status == cls.TRANSIENT_ERROR:
    raise TransientReplicaUpdateError(
        'Transient error (error code %d).' % response.error_code)
  if response.status == cls.FATAL_ERROR:
    raise FatalReplicaUpdateError(
        'Fatal error (error code %d).' % response.error_code)
  if response.status not in (cls.APPLIED, cls.SKIPPED):
    raise FatalReplicaUpdateError(
        'Unexpected response status: %d' % response.status)

  # Replica applied the update, current_revision should be set.
  if not response.HasField('current_revision'):
    raise FatalReplicaUpdateError(
        'Incomplete response, current_revision is missing')

  # Extract auth component version used by replica if proto is recent enough.
  auth_code_version = None
  if response.HasField('auth_code_version'):
    auth_code_version = response.auth_code_version

  raise ndb.Return((response.current_revision, auth_code_version))
Ejemplo n.º 8
0
def push_to_replica(replica_url, auth_db_blob, key_name, sig):
    """Pushes |auth_db_blob| to a replica via URLFetch POST.

  Args:
    replica_url: root URL of a replica (i.e. https://<host>).
    auth_db_blob: binary blob with serialized Auth DB.
    key_name: name of a RSA key used to generate a signature.
    sig: base64 encoded signature of |auth_db_blob|.

  Returns:
    Tuple:
      AuthDB revision reporter by a replica (as replication_pb2.AuthDBRevision).
      Auth component version used by replica (see components.auth.version).

  Raises:
    FatalReplicaUpdateError if replica rejected the push.
    TransientReplicaUpdateError if push should be retried.
  """
    replica_url = replica_url.rstrip('/')
    logging.info('Updating replica %s', replica_url)
    protocol = 'http://' if utils.is_local_dev_server() else 'https://'
    assert replica_url.startswith(protocol)

    # Pass signature via the headers.
    headers = {
        'Content-Type': 'application/octet-stream',
        'X-URLFetch-Service-Id': utils.get_urlfetch_service_id(),
        'X-AuthDB-SigKey-v1': key_name,
        'X-AuthDB-SigVal-v1': sig,
    }

    # On dev appserver emulate X-Appengine-Inbound-Appid header.
    if utils.is_local_dev_server():
        headers['X-Appengine-Inbound-Appid'] = app_identity.get_application_id(
        )

    # 'follow_redirects' set to False is required for 'X-Appengine-Inbound-Appid'
    # to work. 70 sec deadline correspond to 60 sec GAE foreground requests
    # deadline plus 10 seconds to account for URL fetch own lags.
    ctx = ndb.get_context()
    result = yield ctx.urlfetch(url=replica_url +
                                '/auth/api/v1/internal/replication',
                                payload=auth_db_blob,
                                method='POST',
                                headers=headers,
                                follow_redirects=False,
                                deadline=70,
                                validate_certificate=True)

    # Any transport level error is transient.
    if result.status_code != 200:
        raise TransientReplicaUpdateError(
            'Push request failed with HTTP code %d' % result.status_code)

    # Deserialize the response.
    cls = replication_pb2.ReplicationPushResponse
    response = cls.FromString(result.content)
    if not response.HasField('status'):
        raise FatalReplicaUpdateError('Incomplete response, status is missing')

    # Convert errors to exceptions.
    if response.status == cls.TRANSIENT_ERROR:
        raise TransientReplicaUpdateError('Transient error (error code %d).' %
                                          response.error_code)
    if response.status == cls.FATAL_ERROR:
        raise FatalReplicaUpdateError('Fatal error (error code %d).' %
                                      response.error_code)
    if response.status not in (cls.APPLIED, cls.SKIPPED):
        raise FatalReplicaUpdateError('Unexpected response status: %d' %
                                      response.status)

    # Replica applied the update, current_revision should be set.
    if not response.HasField('current_revision'):
        raise FatalReplicaUpdateError(
            'Incomplete response, current_revision is missing')

    # Extract auth component version used by replica if proto is recent enough.
    auth_code_version = None
    if response.HasField('auth_code_version'):
        auth_code_version = response.auth_code_version

    raise ndb.Return((response.current_revision, auth_code_version))
Ejemplo n.º 9
0
def become_replica(ticket, initiated_by):
  """Converts current service to a replica of a primary specified in a ticket.

  Args:
    ticket: replication_pb2.ServiceLinkTicket passed from a primary.
    initiated_by: Identity of a user that accepted linking request, for logging.

  Raises:
    ProtocolError in case the request to primary fails.
  """
  assert model.is_standalone()

  # On dev appserver emulate X-Appengine-Inbound-Appid header.
  headers = {'Content-Type': 'application/octet-stream'}
  protocol = 'https'
  if utils.is_local_dev_server():
    headers['X-Appengine-Inbound-Appid'] = app_identity.get_application_id()
    protocol = 'http'
  headers['X-URLFetch-Service-Id'] = utils.get_urlfetch_service_id()

  # Pass back the ticket for primary to verify it, tell the primary to use
  # default version hostname to talk to us.
  link_request = replication_pb2.ServiceLinkRequest()
  link_request.ticket = ticket.ticket
  link_request.replica_url = (
      '%s://%s' % (protocol, app_identity.get_default_version_hostname()))
  link_request.initiated_by = initiated_by.to_bytes()

  # Primary will look at X-Appengine-Inbound-Appid and compare it to what's in
  # the ticket.
  try:
    result = urlfetch.fetch(
        url='%s/auth_service/api/v1/internal/link_replica' % ticket.primary_url,
        payload=link_request.SerializeToString(),
        method='POST',
        headers=headers,
        follow_redirects=False,
        deadline=30,
        validate_certificate=True)
  except urlfetch.Error as exc:
    raise ProtocolError(
        replication_pb2.ServiceLinkResponse.TRANSPORT_ERROR,
        'URLFetch error (%s): %s' % (exc.__class__.__name__, exc))

  # Protobuf based protocol is not using HTTP codes (handler always replies with
  # HTTP 200, providing error details if needed in protobuf serialized body).
  # So any other status code here means there was a transport level error.
  if result.status_code != 200:
    raise ProtocolError(
        replication_pb2.ServiceLinkResponse.TRANSPORT_ERROR,
        'Request to the primary failed with HTTP %d.' % result.status_code)

  link_response = replication_pb2.ServiceLinkResponse.FromString(result.content)
  if link_response.status != replication_pb2.ServiceLinkResponse.SUCCESS:
    message = LINKING_ERRORS.get(
        link_response.status,
        'Request to the primary failed with status %d.' % link_response.status)
    raise ProtocolError(link_response.status, message)

  # Become replica. Auth DB will be overwritten on a first push from Primary.
  state = model.AuthReplicationState(
      key=model.replication_state_key(),
      primary_id=ticket.primary_id,
      primary_url=ticket.primary_url)
  state.put()