¿Cómo obtener la dirección IP del cliente de un usuario en ASP.NET?


387

Tenemos Request.UserHostAddressque obtener la dirección IP en ASP.NET, pero esta suele ser la dirección IP del ISP del usuario, no exactamente la dirección IP de la máquina del usuario que, por ejemplo, hizo clic en un enlace. ¿Cómo puedo obtener la dirección IP real?

Por ejemplo, en un perfil de usuario de Stack Overflow es: "Última actividad de la cuenta: hace 4 horas desde 86.123.127.8" , pero la dirección IP de mi máquina es un poco diferente. ¿Cómo obtiene Stack Overflow esta dirección?

En algunos sistemas web hay una verificación de dirección IP para algunos propósitos. Por ejemplo, con una determinada dirección IP, por cada 24 horas, ¿puede el usuario tener solo 5 clics en los enlaces de descarga? Esta dirección IP debe ser única, no para un ISP que tenga una gran variedad de clientes o usuarios de Internet.

¿Lo entendí bien?


44
Por lo general, hacen lo mismo y no funcionan correctamente para las direcciones IP compartidas. No se puede hacer mucho en esta área.
Mehrdad

¿Cuál es el problema que está tratando de resolver aquí? ¿Por qué cree que necesita la dirección IP?
Steve

3
Tengo una aplicación que verifica los clics de un enlace específico, y un usuario específico (por IP) no puede hacer clic en el enlace más de 5 veces en un día. El problema es que si Request.UserHostAddress es para un rango de usuarios bajo un ISP o Red o la de un usuario específico?
Mehdi

Respuestas:


142

Como otros han dicho, no puedes hacer lo que pides. Si describe el problema que está tratando de resolver, ¿tal vez alguien pueda ayudarlo?

P.ej

  • ¿Estás tratando de identificar de manera única a tus usuarios?
  • ¿Podría usar una cookie, o la identificación de la sesión, en lugar de la dirección IP?

EditarLa dirección que ve en el servidor no debe ser la dirección del ISP, ya que dice que sería un rango enorme. La dirección para un usuario doméstico en banda ancha será la dirección en su enrutador, por lo que todos los dispositivos dentro de la casa aparecerán en el exterior como iguales, pero el enrutador usa NAT para garantizar que el tráfico se enrute correctamente a cada dispositivo. Para los usuarios que acceden desde un entorno de oficina, la dirección puede ser la misma para todos los usuarios. Los sitios que usan la dirección IP para identificación corren el riesgo de equivocarse: los ejemplos que da son buenos y con frecuencia fallan. Por ejemplo, mi oficina está en el Reino Unido, el punto de ruptura (donde "parezco" estar en Internet) está en otro país donde está nuestra principal instalación de TI, por lo que desde mi oficina mi dirección IP parece no estar en el Reino Unido. Por este motivo, no puedo acceder al contenido web del Reino Unido solamente, como el iPlayer de la BBC). En cualquier momento, habría cientos, o incluso miles, de personas en mi empresa que parecen estar accediendo a la web desde la misma dirección IP.

Cuando escribe código de servidor, nunca puede estar seguro de a qué se refiere la dirección IP que ve. A algunos usuarios les gusta de esta manera. Algunas personas usan deliberadamente un proxy o VPN para confundirlo aún más.

Cuando dice que la dirección de su máquina es diferente a la dirección IP que se muestra en StackOverflow, ¿cómo encuentra la dirección de su máquina? Si solo está buscando usar localmente ipconfigo algo así, esperaría que fuera diferente por las razones que describí anteriormente. Si desea verificar dos veces lo que piensa el mundo exterior, eche un vistazo a whatismyipaddress.com/ .

Este enlace de Wikipedia en NAT le proporcionará algunos antecedentes sobre esto.


así que entendí en las aplicaciones del lado del servidor que no podemos estar seguros acerca de la dirección IP. Significa que la programación del lado del cliente es la solución? quieres decir, por ejemplo, con algunos códigos JavaScript podemos hacer eso?
Mehdi

12
NO, esto no tendría sentido, la dirección IP que el cliente "cree" que tiene será interna al hogar u oficina, no tendrá sentido en el mundo exterior. Por ejemplo, la mayoría de los enrutadores domésticos distribuyen direcciones IP en el rango 192.168.1.xxx, por lo que miles de máquinas tienen la misma dirección en sus propias redes.
Steve

12
No, no es único. Dos usuarios detrás del mismo enrutador que usan NAT tendrán la misma dirección IP. Realmente creo que necesitas leer sobre esto, ver el enlace en mi edición.
Steve

1
Entonces, ¿por qué las empresas como AWS, Azure, etc. utilizan la dirección IP en las reglas del grupo de seguridad y solo permiten que esa dirección IP se conecte a la VM?

2
@ user5950947: Porque Azure espera que sea una empresa con una dirección IP pública estática. Es seguro asumir que su empresa solo tendrá acceso desde su dirección IP pública, por lo que es una buena característica de seguridad adicional. Pero las direcciones IP pueden ser falsificadas o su red puede ser pirateada, por lo que nunca debería ser la única seguridad.
Deantwo

449

A menudo, querrá saber la dirección IP de alguien que visita su sitio web. Si bien ASP.NET tiene varias formas de hacer esto, una de las mejores formas que hemos visto es mediante el uso de "HTTP_X_FORWARDED_FOR" de la colección ServerVariables.

Este es el por qué...

Algunas veces sus visitantes están detrás de un servidor proxy o un enrutador y el estándar Request.UserHostAddresssolo captura la dirección IP del servidor proxy o enrutador. En este caso, la dirección IP del usuario se almacena en la variable del servidor ("HTTP_X_FORWARDED_FOR").

Entonces, lo que queremos hacer es primero verificar "HTTP_X_FORWARDED_FOR" y si eso está vacío, simplemente regresamos ServerVariables("REMOTE_ADDR").

Si bien este método no es infalible, puede conducir a mejores resultados. A continuación se muestra el código ASP.NET en VB.NET, tomado de la publicación del blog de James Crowley "Gotcha: HTTP_X_FORWARDED_FOR devuelve múltiples direcciones IP"

C#

protected string GetIPAddress()
{
    System.Web.HttpContext context = System.Web.HttpContext.Current; 
    string ipAddress = context.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

    if (!string.IsNullOrEmpty(ipAddress))
    {
        string[] addresses = ipAddress.Split(',');
        if (addresses.Length != 0)
        {
            return addresses[0];
        }
    }

    return context.Request.ServerVariables["REMOTE_ADDR"];
}

VB.NET

Public Shared Function GetIPAddress() As String
    Dim context As System.Web.HttpContext = System.Web.HttpContext.Current
    Dim sIPAddress As String = context.Request.ServerVariables("HTTP_X_FORWARDED_FOR")
    If String.IsNullOrEmpty(sIPAddress) Then
        Return context.Request.ServerVariables("REMOTE_ADDR")
    Else
        Dim ipArray As String() = sIPAddress.Split(New [Char]() {","c})
        Return ipArray(0)
    End If
End Function

24
Asegúrese de no usar este código por razones de seguridad porque cualquiera puede falsificar HTTP_X_FORWARDED_FOR o encabezados similares. Por lo tanto, si utiliza esto para el registro o las comprobaciones de seguridad relacionadas con la seguridad, un atacante puede omitirlo fácilmente.
dr. mal

44
Desde su enlace, what we actually needed to be doing was take the last IP addresspero su código obtiene el primero addresses[0]. ¿Cual es correcta?
Nelson Rothermel

44
@NelsonRothermel Basado en en.wikipedia.org/wiki/X-Forwards-For#Format si desea el cliente (en lugar del proxy anterior), use el primero .
Richard

44
@ dr.evil Entonces, ¿qué propondrías en su lugar? Porque quiero establecer el acceso a los servicios WCF por IP especificadas.
krypru

77
addresses.Length != 0no es necesario, ya que nunca puede ser 0.
James Wilkins

78

ACTUALIZACIÓN: Gracias a Bruno Lopes. Si pueden venir varias direcciones IP, entonces necesita usar este método:

    private string GetUserIP()
    {
        string ipList = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

        if (!string.IsNullOrEmpty(ipList))
        {
            return ipList.Split(',')[0];
        }

        return Request.ServerVariables["REMOTE_ADDR"];
    }

3
Como se señaló en otras respuestas, HTTP_X_FORWARDED_FOR puede ser una lista de IP, separadas por comas.
Bruno Lopes

En respuesta a la función solo obtengo :: 1 cada vez. ¿No puedo obtener la dirección IP completa?
farhangdon

1
devolverá :: 1 en el host local. pruébalo en un entorno productino y debería estar bien.
Benny Margalit

1
@farhangdon, el siguiente código devolverá la dirección IP en el host local como @Bat_Programmer escribió a continuaciónSystem.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList[1].ToString();
Benny Margalit

¡NO HAGAS ESTO! Tenga cuidado de no usar simplemente la primera dirección IP de la lista. Solo debe omitir las direcciones IP de proxy conocidas que comienzan en la entrada más a la derecha para evitar ataques de hombre en el medio y suplantación de encabezados.
Jpsy

24

Si es c # mira de esta manera, es muy simple

string clientIp = (Request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? 
                   Request.ServerVariables["REMOTE_ADDR"]).Split(',')[0].Trim();

8
Si ambas variables del servidor pueden ser nulas, puede arrojar una excepción.
Michael Freidgeim

¿Qué es Solicitar referencia aquí? I ServerVariables no encontrado
Brownbagger11

23

¿Qué más considera la dirección IP del usuario? Si desea la dirección IP del adaptador de red, me temo que no hay forma posible de hacerlo en una aplicación web. Si su usuario está detrás de NAT u otras cosas, tampoco puede obtener la IP.

Actualización : si bien hay sitios web que usan IP para limitar al usuario (como rapidshare), no funcionan correctamente en entornos NAT.


22

Creo que debería compartir mi experiencia con todos ustedes. Bueno, veo que en algunas situaciones REMOTE_ADDR NO le conseguirá lo que está buscando. Por ejemplo, si tiene un Load Balancer detrás de escena y si está tratando de obtener la IP del Cliente, entonces estará en problemas. Lo verifiqué con mi software de enmascaramiento de IP y también con mis colegas que se encuentran en diferentes continentes. Entonces aquí está mi solución.

Cuando quiero conocer la IP de un cliente, trato de elegir todas las pruebas posibles para poder determinar si son únicas:

Aquí encontré otro sever-var que podría ayudarlos a todos si desean obtener la IP exacta del lado del cliente. entonces estoy usando: HTTP_X_CLUSTER_CLIENT_IP

HTTP_X_CLUSTER_CLIENT_IP siempre le proporciona la IP exacta del cliente. En cualquier caso, si no le está dando el valor, debe buscar HTTP_X_FORWARDED_FOR ya que es el segundo mejor candidato para obtener la IP del cliente y luego la var REMOTE_ADDR que puede devolverle la IP o no, pero para mí tener todo esto tres es lo que mejor encuentro para monitorearlos.

Espero que esto ayude a algunos chicos.


Es necesario decir que los encabezados http_x _... pueden ser fácilmente falsificados con respecto a la variable remote_addr. Y, por lo tanto, remote_addr sigue siendo la fuente más confiable para la dirección IP del cliente.
Ciro Corvino

1
@CiroCorvino tiene razón, pero cuando tiene su sitio web alojado en un servidor que se ejecuta detrás de Load Balancer (como ya he mencionado en mi publicación), remote_addr no le dará la IP que está buscando. He experimentado lo que estás diciendo, pero cuando busqué la solución, lo que he dicho funciona mejor para mí.
KMX

Lo siento @KMX, en efecto, también uso una combinación de remote_addr y http_x_forward para inferir algunas suposiciones sobre el origen de la solicitud. Si edita su publicación, actualizo mi voto
Ciro Corvino

15

Puedes usar:

System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList.GetValue(0).ToString();

1
gracias, para mí este es el único método que realmente devuelve mi IP en el host local.
Benny Margalit

En realidad, lo que he usado para obtener la dirección IP del host local esSystem.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList[1].ToString();
Benny Margalit

1
Este código solo funciona para mí y puedo obtener IPv6 e IPv4 usando GetValue (0) y GetValue (1) respectivamente. Gracias Upvoted!
Raj Baral

Sin embargo, recibo un error al ejecutar eso en Request for the permission of type 'System.Net.DnsPermission, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
Fiddle

OP preguntando cómo obtener la IP remota del usuario web, no la dirección de su propio localhost.
Hacia el

13

Las direcciones IP son parte de la capa de red en la "pila de siete capas". La capa de red puede hacer lo que quiera con la dirección IP. Eso es lo que sucede con un servidor proxy, NAT, retransmisión o lo que sea.

La capa de aplicación no debe depender de la dirección IP de ninguna manera. En particular, una dirección IP no pretende ser un identificador de otra cosa que no sea el identificador de un extremo de una conexión de red. Tan pronto como se cierra una conexión, debe esperar que cambie la dirección IP (del mismo usuario).


1
Eso está muy bien, pero ¿qué haces cuando un cliente de un sistema de múltiples inquilinos exige que las cuentas de sus usuarios solo puedan iniciar sesión desde una dirección IP específica?
Ronnie Overby

1
Luego tienen que decirle qué dirección IP verá su servidor. Si necesitan que haya una dirección particular, no podrán estar detrás de un NAT o similar.
John Saunders

@RonnieOverby Estoy en la misma situación. Necesito saber desde qué IP se están conectando y debe estar en mi lista blanca. Luego, la aplicación puede activar o desactivar ciertas funciones en función de su IP. Esto es lo que quiere el cliente.
T.durden 01 de

10

Todas las respuestas hasta ahora tienen en cuenta el encabezado no estandarizado, pero muy común X-Forwarded-For. Hay un Forwardedencabezado estandarizado que es un poco más difícil de analizar. Algunos ejemplos son los siguientes:

Forwarded: for="_gazonk"
Forwarded: For="[2001:db8:cafe::17]:4711"
Forwarded: for=192.0.2.60;proto=http;by=203.0.113.43
Forwarded: for=192.0.2.43, for=198.51.100.17

He escrito una clase que tiene en cuenta estos dos encabezados al determinar la dirección IP de un cliente.

using System;
using System.Web;

namespace Util
{
    public static class IP
    {
        public static string GetIPAddress()
        {
            return GetIPAddress(new HttpRequestWrapper(HttpContext.Current.Request));
        }

        internal static string GetIPAddress(HttpRequestBase request)
        {
            // handle standardized 'Forwarded' header
            string forwarded = request.Headers["Forwarded"];
            if (!String.IsNullOrEmpty(forwarded))
            {
                foreach (string segment in forwarded.Split(',')[0].Split(';'))
                {
                    string[] pair = segment.Trim().Split('=');
                    if (pair.Length == 2 && pair[0].Equals("for", StringComparison.OrdinalIgnoreCase))
                    {
                        string ip = pair[1].Trim('"');

                        // IPv6 addresses are always enclosed in square brackets
                        int left = ip.IndexOf('['), right = ip.IndexOf(']');
                        if (left == 0 && right > 0)
                        {
                            return ip.Substring(1, right - 1);
                        }

                        // strip port of IPv4 addresses
                        int colon = ip.IndexOf(':');
                        if (colon != -1)
                        {
                            return ip.Substring(0, colon);
                        }

                        // this will return IPv4, "unknown", and obfuscated addresses
                        return ip;
                    }
                }
            }

            // handle non-standardized 'X-Forwarded-For' header
            string xForwardedFor = request.Headers["X-Forwarded-For"];
            if (!String.IsNullOrEmpty(xForwardedFor))
            {
                return xForwardedFor.Split(',')[0];
            }

            return request.UserHostAddress;
        }
    }
}

A continuación se presentan algunas pruebas unitarias que utilicé para validar mi solución:

using System.Collections.Specialized;
using System.Web;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UtilTests
{
    [TestClass]
    public class IPTests
    {
        [TestMethod]
        public void TestForwardedObfuscated()
        {
            var request = new HttpRequestMock("for=\"_gazonk\"");
            Assert.AreEqual("_gazonk", Util.IP.GetIPAddress(request));
        }

        [TestMethod]
        public void TestForwardedIPv6()
        {
            var request = new HttpRequestMock("For=\"[2001:db8:cafe::17]:4711\"");
            Assert.AreEqual("2001:db8:cafe::17", Util.IP.GetIPAddress(request));
        }

        [TestMethod]
        public void TestForwardedIPv4()
        {
            var request = new HttpRequestMock("for=192.0.2.60;proto=http;by=203.0.113.43");
            Assert.AreEqual("192.0.2.60", Util.IP.GetIPAddress(request));
        }

        [TestMethod]
        public void TestForwardedIPv4WithPort()
        {
            var request = new HttpRequestMock("for=192.0.2.60:443;proto=http;by=203.0.113.43");
            Assert.AreEqual("192.0.2.60", Util.IP.GetIPAddress(request));
        }

        [TestMethod]
        public void TestForwardedMultiple()
        {
            var request = new HttpRequestMock("for=192.0.2.43, for=198.51.100.17");
            Assert.AreEqual("192.0.2.43", Util.IP.GetIPAddress(request));
        }
    }

    public class HttpRequestMock : HttpRequestBase
    {
        private NameValueCollection headers = new NameValueCollection();

        public HttpRequestMock(string forwarded)
        {
            headers["Forwarded"] = forwarded;
        }

        public override NameValueCollection Headers
        {
            get { return this.headers; }
        }
    }
}

9

Si está utilizando CloudFlare, puede probar este Método de extensión :

public static class IPhelper
{
    public static string GetIPAddress(this HttpRequest Request)
    {
        if (Request.Headers["CF-CONNECTING-IP"] != null) return Request.Headers["CF-CONNECTING-IP"].ToString();

        if (Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null) return Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString();

        return Request.UserHostAddress;
    }
}

entonces

string IPAddress = Request.GetIPAddress();

¿Y con F5 o Palo Alto?
Kiquenet

8
string IP = HttpContext.Current.Request.Params["HTTP_CLIENT_IP"] ?? HttpContext.Current.Request.UserHostAddress;

7

Lo que puede hacer es almacenar la IP del enrutador de su usuario y también la IP reenviada e intentar que sea confiable utilizando las IP [Public Public y Internal Private]. Pero nuevamente, después de algunos días, se le puede asignar al cliente una nueva IP interna desde el enrutador, pero será más confiable.


5

Combinando las respuestas de @Tony y @mangokun , he creado el siguiente método de extensión:

public static class RequestExtensions
{
    public static string GetIPAddress(this HttpRequest Request)
    {
        if (Request.Headers["CF-CONNECTING-IP"] != null) return Request.Headers["CF-CONNECTING-IP"].ToString();

        if (Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null)
        {
            string ipAddress = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

            if (!string.IsNullOrEmpty(ipAddress))
            {
                string[] addresses = ipAddress.Split(',');
                if (addresses.Length != 0)
                {
                    return addresses[0];
                }
            }
        }

        return Request.UserHostAddress;
    }
}

¿Por qué usas HTTP_X_FORWARDED_FORpero no X_FORWARDED_FOR? ¿Son lo mismo?
Igor Yalovoy

3

utilizar en archivo ashx

public string getIP(HttpContext c)
{
    string ips = c.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
    if (!string.IsNullOrEmpty(ips))
    {
        return ips.Split(',')[0];
    }
    return c.Request.ServerVariables["REMOTE_ADDR"];
}

2
public static class Utility
{
    public static string GetClientIP(this System.Web.UI.Page page)
    {
        string _ipList = page.Request.Headers["CF-CONNECTING-IP"].ToString();
        if (!string.IsNullOrWhiteSpace(_ipList))
        {
            return _ipList.Split(',')[0].Trim();
        }
        else
        {
            _ipList = page.Request.ServerVariables["HTTP_X_CLUSTER_CLIENT_IP"];
            if (!string.IsNullOrWhiteSpace(_ipList))
            {
                return _ipList.Split(',')[0].Trim();
            }
            else
            {
                _ipList = page.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
                if (!string.IsNullOrWhiteSpace(_ipList))
                {
                    return _ipList.Split(',')[0].Trim();
                }
                else
                {
                    return page.Request.ServerVariables["REMOTE_ADDR"].ToString().Trim();
                }
            }
        }
    }
}

Utilizar;

string _ip = this.GetClientIP();

0

Es fácil. Pruébalo:

var remoteIpAddress = Request.HttpContext.Connection.RemoteIpAddress;

solo esto :))


1
Para su información, el código anterior solo funciona en Asp.Net Core y no en el enlace
Dobin

-1

Hola chicos La mayoría de los códigos que encontrarás te devolverán la dirección IP del servidor, no la dirección IP del cliente. Sin embargo, este código devuelve la dirección IP correcta del cliente. Pruébalo. Para más información solo marque esto

https://www.youtube.com/watch?v=Nkf37DsxYjI

para obtener su dirección IP local usando JavaScript, puede usar poner este código dentro de su etiqueta de script

<script>
    var RTCPeerConnection = /*window.RTCPeerConnection ||*/
     window.webkitRTCPeerConnection || window.mozRTCPeerConnection;

         if (RTCPeerConnection) (function () {
             var rtc = new RTCPeerConnection({ iceServers: [] });
             if (1 || window.mozRTCPeerConnection) {      
                 rtc.createDataChannel('', { reliable: false });
             };

             rtc.onicecandidate = function (evt) {

                 if (evt.candidate)
                     grepSDP("a=" + evt.candidate.candidate);
             };
             rtc.createOffer(function (offerDesc) {
                 grepSDP(offerDesc.sdp);
                 rtc.setLocalDescription(offerDesc);
             }, function (e) { console.warn("offer failed", e); });


             var addrs = Object.create(null);
             addrs["0.0.0.0"] = false;
             function updateDisplay(newAddr) {
                 if (newAddr in addrs) return;
                 else addrs[newAddr] = true;
                 var displayAddrs = Object.keys(addrs).filter(function
(k) { return addrs[k]; });
                 document.getElementById('list').textContent =
displayAddrs.join(" or perhaps ") || "n/a";
             }

             function grepSDP(sdp) {
                 var hosts = [];
                 sdp.split('\r\n').forEach(function (line) { 
                     if (~line.indexOf("a=candidate")) {   
                         var parts = line.split(' '),   
                             addr = parts[4],
                             type = parts[7];
                         if (type === 'host') updateDisplay(addr);
                     } else if (~line.indexOf("c=")) {      
                         var parts = line.split(' '),
                             addr = parts[2];
                         updateDisplay(addr);
                     }
                 });
             }
         })(); else
         {
             document.getElementById('list').innerHTML = "<code>ifconfig| grep inet | grep -v inet6 | cut -d\" \" -f2 | tail -n1</code>";
             document.getElementById('list').nextSibling.textContent = "In Chrome and Firefox your IP should display automatically, by the power of WebRTCskull.";

         }




</script>
<body>
<div id="list"></div>
</body>

y Para obtener su dirección IP pública, puede usar poner este código dentro de su etiqueta de script

  function getIP(json) {
    document.write("My public IP address is: ", json.ip);
  }


<script type="application/javascript" src="https://api.ipify.org?format=jsonp&callback=getIP"></script>


-7

Tratar:

using System.Net;

public static string GetIpAddress()  // Get IP Address
{
    string ip = "";     
    IPHostEntry ipEntry = Dns.GetHostEntry(GetCompCode());
    IPAddress[] addr = ipEntry.AddressList;
    ip = addr[2].ToString();
    return ip;
}
public static string GetCompCode()  // Get Computer Name
{   
    string strHostName = "";
    strHostName = Dns.GetHostName();
    return strHostName;
}

Esto devuelve la dirección IP del servidor
Sreekumar P

Se trata de asp.net, que es una aplicación web, que se ejecuta en un servidor, no en la computadora del usuario.
Doekman
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.