[#7800] factor out a common method for ip address lookup from a request
diff --git a/Allura/allura/lib/decorators.py b/Allura/allura/lib/decorators.py
index 28ad5bf..d472758 100644
--- a/Allura/allura/lib/decorators.py
+++ b/Allura/allura/lib/decorators.py
@@ -25,11 +25,11 @@
 from decorator import decorator
 from tg.decorators import before_validate
 from tg import request, redirect
-
 from webob import exc
-
 from pylons import tmpl_context as c
+
 from allura.lib import helpers as h
+from allura.lib import utils
 
 
 def task(*args, **kw):
@@ -161,9 +161,7 @@
         '''
         extra = self._extra_proto.copy()
         # Save the client IP address
-        client_ip = request.headers.get('X_FORWARDED_FOR', request.remote_addr)
-        client_ip = client_ip.split(',')[0].strip()
-        extra.update(client_ip=client_ip)
+        extra.update(client_ip=utils.ip_address(request))
         # Save the user info
         user = getattr(request, 'user', None)
         if user:
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 981534c..3b6a1d5 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -59,10 +59,13 @@
 from webob.exc import HTTPUnauthorized
 
 from allura.lib import exceptions as exc
-# Reimport to make available to templates
 from allura.lib import AsciiDammit
+from allura.lib import utils
+
+# import to make available to templates, don't delete:
 from .security import has_access
 
+
 log = logging.getLogger(__name__)
 
 # validates project, subproject, and user names
@@ -657,13 +660,7 @@
                 result['username'] = '*system'
             try:
                 result['url'] = request.url
-                ip_address = request.headers.get(
-                    'X_FORWARDED_FOR', request.remote_addr)
-                if ip_address is not None:
-                    ip_address = ip_address.split(',')[0].strip()
-                    result['ip_address'] = ip_address
-                else:
-                    result['ip_address'] = '0.0.0.0'
+                result['ip_address'] = utils.ip_address(request)
             except TypeError:
                 pass
             return result
@@ -1206,7 +1203,7 @@
     :param user: a :class:`allura.model.auth.User`
     """
     from allura import model as M
-    ip_address = request.headers.get('X-Remote-Addr', request.remote_addr)
+    ip_address = utils.ip_address(request)
     message = 'IP Address: {}\n'.format(ip_address) + message
     if kwargs.get('user') and kwargs['user'] != c.user:
         message = 'Done by user: {}\n'.format(c.user.username) + message
diff --git a/Allura/allura/lib/spam/akismetfilter.py b/Allura/allura/lib/spam/akismetfilter.py
index 775aecc..4913970 100644
--- a/Allura/allura/lib/spam/akismetfilter.py
+++ b/Allura/allura/lib/spam/akismetfilter.py
@@ -21,6 +21,7 @@
 from pylons import tmpl_context as c
 
 from allura.lib import helpers as h
+from allura.lib import utils
 from allura.lib.spam import SpamFilter
 
 try:
@@ -66,8 +67,7 @@
             kw['comment_author'] = user.display_name or user.username
             kw['comment_author_email'] = user.email_addresses[
                 0] if user.email_addresses else ''
-        user_ip = request.headers.get('X_FORWARDED_FOR', request.remote_addr)
-        kw['user_ip'] = user_ip.split(',')[0].strip()
+        kw['user_ip'] = utils.ip_address(request)
         kw['user_agent'] = request.headers.get('USER_AGENT')
         kw['referrer'] = request.headers.get('REFERER')
         # kw will be urlencoded, need to utf8-encode
diff --git a/Allura/allura/lib/spam/mollomfilter.py b/Allura/allura/lib/spam/mollomfilter.py
index d76ff59..3651a94 100644
--- a/Allura/allura/lib/spam/mollomfilter.py
+++ b/Allura/allura/lib/spam/mollomfilter.py
@@ -21,6 +21,7 @@
 from pylons import tmpl_context as c
 
 from allura.lib import helpers as h
+from allura.lib import utils
 from allura.lib.spam import SpamFilter
 
 try:
@@ -75,8 +76,7 @@
             kw['authorName'] = user.display_name or user.username
             kw['authorMail'] = user.email_addresses[
                 0] if user.email_addresses else ''
-        user_ip = request.headers.get('X_FORWARDED_FOR', request.remote_addr)
-        kw['authorIP'] = user_ip.split(',')[0].strip()
+        kw['authorIP'] = utils.ip_address(request)
         # kw will be urlencoded, need to utf8-encode
         for k, v in kw.items():
             kw[k] = h.really_unicode(v).encode('utf8')
diff --git a/Allura/allura/lib/utils.py b/Allura/allura/lib/utils.py
index ce1836f..35bc039 100644
--- a/Allura/allura/lib/utils.py
+++ b/Allura/allura/lib/utils.py
@@ -336,9 +336,7 @@
         if timestamp is None:
             timestamp = self.timestamp
         try:
-            client_ip = self.request.headers.get(
-                'X_FORWARDED_FOR', self.request.remote_addr)
-            client_ip = client_ip.split(',')[0].strip()
+            client_ip = ip_address(self.request)
         except (TypeError, AttributeError), err:
             client_ip = '127.0.0.1'
         plain = '%d:%s:%s' % (
@@ -552,3 +550,9 @@
                 self.allowed_elements.append('iframe')
         return super(ForgeHTMLSanitizer, self).sanitize_token(token)
 
+
+def ip_address(request):
+    ip = request.remote_addr
+    if tg.config.get('ip_address_header'):
+        ip = request.headers.get(tg.config['ip_address_header']) or ip
+    return ip
\ No newline at end of file
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 4632301..daba6c3 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -31,6 +31,7 @@
 
 from allura.lib import helpers as h
 from allura.lib import security
+from allura.lib import utils
 
 from allura.lib.search import SearchIndexable
 from .session import main_orm_session
@@ -472,9 +473,7 @@
     def commit(self, update_stats=True):
         '''Save off a snapshot of the artifact and increment the version #'''
         try:
-            ip_address = request.headers.get(
-                'X_FORWARDED_FOR', request.remote_addr)
-            ip_address = ip_address.split(',')[0].strip()
+            ip_address = utils.ip_address(request)
         except:
             ip_address = '0.0.0.0'
         data = dict(
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 7928796..6217f42 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -42,6 +42,7 @@
 import allura.tasks.mail_tasks
 from allura.lib import helpers as h
 from allura.lib import plugin
+from allura.lib import utils
 from allura.lib.decorators import memoize
 from allura.lib.search import SearchIndexable
 from .session import main_orm_session, main_doc_session
@@ -347,7 +348,7 @@
         return dict(provider.index_user(self), **fields)
 
     def track_login(self, req):
-        user_ip = req.headers.get('X_FORWARDED_FOR', req.remote_addr)
+        user_ip = utils.ip_address(req)
         user_agent = req.headers.get('User-Agent')
         self.last_access['login_date'] = datetime.utcnow()
         self.last_access['login_ip'] = user_ip
@@ -355,7 +356,7 @@
         session(self).flush(self)
 
     def track_active(self, req):
-        user_ip = req.headers.get('X_FORWARDED_FOR', req.remote_addr)
+        user_ip = utils.ip_address(req)
         user_agent = req.headers.get('User-Agent')
         now = datetime.utcnow()
         last_date = self.last_access['session_date']
diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py
index 2206f37..9d0f1b1 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -81,7 +81,8 @@
         assert_equal(user.last_access['login_ua'], None)
 
         self.app.post('/auth/do_login',
-                      headers={'X_FORWARDED_FOR': 'addr', 'User-Agent': 'browser'},
+                      headers={'User-Agent': 'browser'},
+                      extra_environ={'REMOTE_ADDR': 'addr'},
                       params=dict(
                           username='test-user',
                           password='foo'
diff --git a/Allura/allura/tests/test_utils.py b/Allura/allura/tests/test_utils.py
index 5e4135b..209d0df 100644
--- a/Allura/allura/tests/test_utils.py
+++ b/Allura/allura/tests/test_utils.py
@@ -21,17 +21,18 @@
 import unittest
 from os import path
 
-import pylons
 from webob import Request
 from mock import Mock, patch
 from nose.tools import assert_equal
 from pygments import highlight
 from pygments.lexers import get_lexer_for_filename
+from tg import config
 
 from alluratest.controller import setup_unit_test
 
 from allura import model as M
 from allura.lib import utils
+from allura.lib import helpers as h
 
 
 @patch.dict('allura.lib.utils.tg.config', clear=True, foo='bar', baz='true')
@@ -96,7 +97,6 @@
 
     def setUp(self):
         setup_unit_test()
-        pylons.request.remote_addr = '127.0.0.1'
         self.a = utils.AntiSpam()
 
     def test_generate_fields(self):
@@ -105,12 +105,6 @@
         assert 'name="spinner"' in fields, fields
         assert ('class="%s"' % self.a.honey_class) in fields, fields
 
-    def test_valid_submit(self):
-        form = dict(a='1', b='2')
-        r = Request.blank('/', POST=self._encrypt_form(**form))
-        validated = utils.AntiSpam.validate_request(r)
-        assert dict(a='1', b='2') == validated, validated
-
     def test_invalid_old(self):
         form = dict(a='1', b='2')
         r = Request.blank('/', POST=self._encrypt_form(**form))
@@ -119,6 +113,13 @@
             utils.AntiSpam.validate_request,
             r, now=time.time() + 24 * 60 * 60 + 1)
 
+    def test_valid_submit(self):
+        form = dict(a='1', b='2')
+        r = Request.blank('/', POST=self._encrypt_form(**form),
+                          environ={'remote_addr': '127.0.0.1'})
+        validated = utils.AntiSpam.validate_request(r)
+        assert dict(a='1', b='2') == validated, validated
+
     def test_invalid_future(self):
         form = dict(a='1', b='2')
         r = Request.blank('/', POST=self._encrypt_form(**form))
@@ -253,3 +254,27 @@
         p = utils.ForgeHTMLSanitizer('<div><iframe src="https://www.youtube.com/embed/kOLpSPEA72U?feature=oembed"></iframe></div>')
         assert_equal(
             self.simple_tag_list(p), ['div', 'iframe', 'div'])
+
+
+def test_ip_address():
+    req = Mock()
+    req.remote_addr = '1.2.3.4'
+    req.headers = {}
+    assert_equal(utils.ip_address(req),
+                 '1.2.3.4')
+
+def test_ip_address_header():
+    req = Mock()
+    req.remote_addr = '1.2.3.4'
+    req.headers = {'X_FORWARDED_FOR': '5.6.7.8'}
+    with h.push_config(config, **{'ip_address_header': 'X_FORWARDED_FOR'}):
+        assert_equal(utils.ip_address(req),
+                     '5.6.7.8')
+
+def test_ip_address_header_not_set():
+    req = Mock()
+    req.remote_addr = '1.2.3.4'
+    req.headers = {}
+    with h.push_config(config, **{'ip_address_header': 'X_FORWARDED_FOR'}):
+        assert_equal(utils.ip_address(req),
+                     '1.2.3.4')
diff --git a/Allura/allura/tests/unit/spam/__init__.py b/Allura/allura/tests/unit/spam/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Allura/allura/tests/unit/spam/__init__.py
diff --git a/Allura/allura/tests/unit/spam/test_akismet.py b/Allura/allura/tests/unit/spam/test_akismet.py
index 1b7215a..79802c3 100644
--- a/Allura/allura/tests/unit/spam/test_akismet.py
+++ b/Allura/allura/tests/unit/spam/test_akismet.py
@@ -41,8 +41,6 @@
         self.fake_user = mock.Mock(display_name=u'Søme User',
                                    email_addresses=['user@domain'])
         self.fake_headers = dict(
-            REMOTE_ADDR='fallback ip',
-            X_FORWARDED_FOR='some ip',
             USER_AGENT='some browser',
             REFERER='some url')
         self.content = u'spåm text'
@@ -57,6 +55,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_check(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.service.comment_check.side_effect({'side_effect': ''})
         self.akismet.check(self.content)
@@ -68,6 +67,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_check_with_explicit_content_type(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.check(self.content, content_type='some content type')
         self.expected_data['comment_type'] = 'some content type'
@@ -79,6 +79,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_check_with_artifact(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.check(self.content, artifact=self.fake_artifact)
         expected_data = self.expected_data
@@ -91,6 +92,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_check_with_user(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.check(self.content, user=self.fake_user)
         expected_data = self.expected_data
@@ -104,6 +106,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_check_with_implicit_user(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = self.fake_user
         self.akismet.check(self.content)
         expected_data = self.expected_data
@@ -115,21 +118,9 @@
 
     @mock.patch('allura.lib.spam.akismetfilter.c')
     @mock.patch('allura.lib.spam.akismetfilter.request')
-    def test_check_with_fallback_ip(self, request, c):
-        self.expected_data['user_ip'] = 'fallback ip'
-        self.fake_headers.pop('X_FORWARDED_FOR')
-        request.headers = self.fake_headers
-        request.remote_addr = self.fake_headers['REMOTE_ADDR']
-        c.user = None
-        self.akismet.check(self.content)
-        self.akismet.service.comment_check.assert_called_once_with(
-            self.content,
-            data=self.expected_data, build_data=False)
-
-    @mock.patch('allura.lib.spam.akismetfilter.c')
-    @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_submit_spam(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.submit_spam(self.content)
         self.akismet.service.submit_spam.assert_called_once_with(
@@ -139,6 +130,7 @@
     @mock.patch('allura.lib.spam.akismetfilter.request')
     def test_submit_ham(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.akismet.submit_ham(self.content)
         self.akismet.service.submit_ham.assert_called_once_with(
diff --git a/Allura/allura/tests/unit/spam/test_mollom.py b/Allura/allura/tests/unit/spam/test_mollom.py
index 931f8a3..cac2ab8 100644
--- a/Allura/allura/tests/unit/spam/test_mollom.py
+++ b/Allura/allura/tests/unit/spam/test_mollom.py
@@ -44,8 +44,6 @@
         self.fake_user = mock.Mock(display_name=u'Søme User',
                                    email_addresses=['user@domain'])
         self.fake_headers = dict(
-            REMOTE_ADDR='fallback ip',
-            X_FORWARDED_FOR='some ip',
             USER_AGENT='some browser',
             REFERER='some url')
         self.content = u'spåm text'
@@ -59,6 +57,7 @@
     @mock.patch('allura.lib.spam.mollomfilter.request')
     def test_check(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.mollom.check(self.content, artifact=self.artifact)
         self.mollom.service.checkContent.assert_called_once_with(
@@ -68,6 +67,7 @@
     @mock.patch('allura.lib.spam.mollomfilter.request')
     def test_check_with_user(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = None
         self.mollom.check(self.content, user=self.fake_user,
                           artifact=self.artifact)
@@ -81,6 +81,7 @@
     @mock.patch('allura.lib.spam.mollomfilter.request')
     def test_check_with_implicit_user(self, request, c):
         request.headers = self.fake_headers
+        request.remote_addr = 'some ip'
         c.user = self.fake_user
         self.mollom.check(self.content, artifact=self.artifact)
         expected_data = self.expected_data
@@ -89,18 +90,6 @@
         self.mollom.service.checkContent.assert_called_once_with(
             **self.expected_data)
 
-    @mock.patch('allura.lib.spam.mollomfilter.c')
-    @mock.patch('allura.lib.spam.mollomfilter.request')
-    def test_check_with_fallback_ip(self, request, c):
-        self.expected_data['authorIP'] = 'fallback ip'
-        self.fake_headers.pop('X_FORWARDED_FOR')
-        request.headers = self.fake_headers
-        request.remote_addr = self.fake_headers['REMOTE_ADDR']
-        c.user = None
-        self.mollom.check(self.content, artifact=self.artifact)
-        self.mollom.service.checkContent.assert_called_once_with(
-            **self.expected_data)
-
     def test_submit_spam(self):
         self.mollom.submit_spam('test', artifact=self.artifact)
         assert self.mollom.service.sendFeedback.call_args[0] == (
diff --git a/Allura/development.ini b/Allura/development.ini
index 0de4e80..e5be8b2 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -159,6 +159,9 @@
 
 ew.extra_headers = [ ('Access-Control-Allow-Origin', '*') ]
 
+; If your environment (e.g. behind a server-side proxy) needs to look at an http header to get the actual remote addr
+;ip_address_header = X-Forwarded-For
+
 # SCM settings for local development
 scm.host.ro.git = /srv/git$path
 scm.host.rw.git = /srv/git$path