#!/usr/bin/env python # # PyAuthenNTLM2: A mod-python module for Apache that carries out NTLM authentication # # pyntlm.py # # Copyright 2011 Legrandin # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys import base64 import time import urllib from struct import unpack from threading import Lock from binascii import hexlify from urlparse import urlparse from mod_python import apache from PyAuthenNTLM2.ntlm_dc_proxy import NTLM_DC_Proxy from PyAuthenNTLM2.ntlm_ad_proxy import NTLM_AD_Proxy use_basic_auth = True try: from PyAuthenNTLM2.ntlm_client import NTLM_Client except ImportError: use_basic_auth = False # # A connection can be in one of the following states when a request arrives: # # 1 Freshly opened: no authentication step has taken place yet. # req.connection.notes does not contain the key 'NTLM_AUTHORIZED' and # the cache does not contain any tuple under the connection's id. # # 2 Pending authentication: we sent the NTLM challenge to the client, and we # are waiting for the response. req.connection.notes does not contain the # the key 'NTLM_AUTHORIZED' but the cache contains one tuple (NTLM_Proxy, timestamp) # under the connection's id. # # 3 Authenticated: all steps completed successfully. # req.connection.notes contains the key 'NTLM_AUTHORIZED' (username) or # 'BASIC_AUTHORIZED' (username and a password). # The cache should not contain any tuple under the connection's id. # # Since connections may be interrupted before receiving the challenge, objects older # than 60 seconds are removed from cache when we have the chance. class CacheConnections: def __init__(self): self._mutex = Lock() self._cache = {} def __len__(self): return len(self._cache) def remove(self, id): self._mutex.acquire() (proxy, ts) = self._cache.get(id, (None,None)) if proxy: proxy.close() del self._cache[id] self._mutex.release() def add(self, id, proxy): self._mutex.acquire() self._cache[id] = ( proxy, int(time.time()) ) self._mutex.release() def clean(self): now = int(time.time()) self._mutex.acquire() for id, conn in self._cache.items(): if conn[1]+600: return apache.OK else: return apache.OK # If there is no Authorization header it means it is the first request. # We reject it with a 401, indicating which authentication protocol we understand. if not auth_headers: return handle_unauthorized(req) # Extract authentication data from any of the Authorization headers try: for ah in auth_headers: ah_data = decode_http_authorization_header(ah) if ah_data: break except: ah_data = False if not ah_data: req.log_error('Error when parsing Authorization header for URI %s' % req.unparsed_uri, apache.APLOG_ERR) return apache.HTTP_BAD_REQUEST if ah_data[0]=='Basic': # If this connection was authenticated with Basic, verify that the # credentials match and return 200 (if they do) or 401 (if they # don't). We don't need to actually query the DC for that. userpwd = req.connection.notes.get('BASIC_AUTHORIZED', None) if userpwd: if userpwd != ah_data[1]+ah_data[2]: return handle_unauthorized(req) domain = req.get_options().get('Domain', req.auth_name()) set_remote_user(req, ah_data[1], domain) return apache.OK # Connection was not authenticated before return handle_basic(req, ah_data[1], ah_data[2]) # If we get here it means that there is an Authorization header, with an # NTLM message in it. Moreover, the connection needs to be (re)authenticated. # Most likely, the NTLM message is of: # - Type 1 (and there is nothing in the cache): the client wants to # authenticate for the first time, # - Type 3 (and there is something in the cache): the client wants to finalize # a pending authentication request. # # However, it could still be that there is a Type 3 and nothing in the # cache (the final client message was erroneously routed to a new connection), # or that there is a Type 1 with something in the cache (the client wants to # initiate an cancel a pending authentication). try: ntlm_version = ntlm_message_type(ah_data[1]) if ntlm_version==1: return handle_type1(req, ah_data[1]) if ntlm_version==3: if cache.has_key(req.connection.id): return handle_type3(req, ah_data[1]) req.log_error('Unexpected NTLM message Type 3 in new connection for URI %s' % (req.unparsed_uri), apache.APLOG_INFO) return handle_unauthorized(req) error = 'Type 2 message in client request' except Exception, e: error = str(e) req.log_error('Incorrect NTLM message in Authorization header for URI %s: %s' % (req.unparsed_uri,error), apache.APLOG_ERR) return apache.HTTP_BAD_REQUEST