SecNothttps://secnot.com/2020-10-08T12:50:00+02:00Leaky Data Diodes2020-10-08T12:50:00+02:002020-10-08T12:50:00+02:00SecNottag:secnot.com,2020-10-08:/leaky-data-diodes-1.html<p class="first last">On data diodes an how some are vulnerable to side-channel attacks.</p>
<p>A data diode is a network device that only allows data to travel in one direction, they are
used primarily in high security applications like nuclear plants, industrial automation, and
armies all over the world. There reason they aren't more widely used, the sacrifice for
their high security is that normal transport protocols don't work over data diodes, as
they require a bidirectional connection.</p>
<p>And here is where everything becomes muddy, because to overcome this limitation some data
diodes use a secondary channel from the isolated side to the untrusted side to send flow
control information back so TCP connection can pass through. This makes them potentially
vulnerable to a <a class="reference external" href="https://en.wikipedia.org/wiki/Covert_channel">covert channel attack</a>,
but there's almost no information on vulnerabilities, attacks, or test tools.</p>
<p>So I wrote <a class="reference external" href="https://github.com/secnot/leaky_diode">leaky diode</a> a test tool to check
if they are resistant to a couple of a very simple attacks.</p>
<div class="section" id="the-attacks">
<h2>The Attacks</h2>
<p>The attacks described in this post require a compromised host on the isolated side, and have
a very low throughput (aroud 1 Bytes/min), so they can't realistically be used to leak more
than a few KB of data.</p>
<p>Regardless of the limitations there're still many fields where even such small leaks
would be catastrophic, and would love to be able to test their diodes so lets dive in.</p>
<div class="section" id="close-delay">
<h3>Close Delay</h3>
<p>Close delay uses the delay between the start of a connection and the time it's closed
by the server to encode the secret bits.</p>
<ul class="simple">
<li><strong>1 (CLIENT)</strong> Open a connection to the server.</li>
<li><strong>2 (CLIENT)</strong> Send a request for one of the secret bits and a threshold delay,
then continue sending junk data to keep the connection alive.</li>
<li><strong>3 (SERVER)</strong> After receiving a request, close the connection inmediately if the value
of the secret bit is 0, or wait threshold delay before closing the connection if the
value is 1.</li>
<li><strong>4 (CLIENT)</strong> If the delay between sending the request to the server and the server
closing the connection is greater than threshold/2 then the bit value is
1, and 0 otherwise.</li>
</ul>
<p>This attack works well in almost all circunstances, as it doesn't require much bandwith
and is not affected by network lattency or buffers in the data diodes. The drawback is
that uses so many connections tha can be easily detected.</p>
</div>
<div class="section" id="flow-modulation">
<h3>Flow Modulation</h3>
<p>Flow modulation uses the flow control mechanisms of TCP to transmit data using the
throughput of a connection as the carrier.</p>
<ul class="simple">
<li><strong>1 (CLIENT)</strong> Connect with the server and set the tx rates to signal low and high bits.
(i.e- 60KB/sec for low 300KB/sec for high bits)</li>
<li><strong>2 (CLIENT)</strong> Send a request for one of the secret's bits, and start sending random
data at a rate greater than the max encoding rate.</li>
<li><strong>3 (SERVER)</strong> On reception of a request start throttling the connection to the
to the speed that encodes the value of the requested bit/bits. (as provided in step 1)</li>
<li><strong>4 (CLIENT)</strong> Wait until the data rate stabilizes/settles and then sample it to obtain the bit
value.</li>
<li><strong>5 (CLIENT)</strong> Go to step 1 until all the secret bits have been read.</li>
</ul>
<p>This attack is harder to detect than close delay as it uses a single connection and not
thousands to exfiltrate the data, the data sent can also be made to mimic another protocol
like http to make it even harder to detect. And with enough care a finely tunned tool could
use very close tx rates for high and low values, so it's indistinguishable from a legitimate
connection.</p>
<p>The drawback is that it's vulnerable to network congestion, QoS, and large TCP stack
buffers that can make it inconsistent.</p>
</div>
<div class="section" id="lower-level-attacks">
<h3>Lower level attacks</h3>
<p>If you have privileges to directly manipulate the stack, there are few more possible attacks
that may work depending on the diode:</p>
<ul class="simple">
<li>Using the ACK delay since the reception of a packet.</li>
<li>Only sending ACK on even or odd sequence numbers, to encode low/high bits.</li>
<li>Manipulating the window size.</li>
</ul>
<p>Some of these are harder to implement and easier to defend against by the diode, but are
great candidades to embed into the OS. Just to be clear I haven't tested any of them.</p>
</div>
</div>
<div class="section" id="test-tool">
<h2>Test tool</h2>
<p>If you have a diode you want to test, download <a class="reference external" href="https://github.com/secnot/leaky_diode">leaky diode</a></p>
<p>On the isolated side launch the server:</p>
<div class="highlight"><pre><span></span><span class="nv">$leaky_server</span> public_ip port <span class="s1">'secret string to leak'</span>
</pre></div>
<p>and on the untrusted side one of:</p>
<div class="highlight"><pre><span></span><span class="nv">$leaky_client</span> server_ip server_port --mode flow --partial
</pre></div>
<p>or</p>
<div class="highlight"><pre><span></span><span class="nv">$leaky_client</span> server_ip server_port --mode close --partial
</pre></div>
<p>Then wait for a few minutes until the first bytes start to arrive, or if you are impacient add
<em>--verbose</em> argument to show notifications on each bit sent/received.</p>
<p>Thank you for reading, and share the results!!!</p>
</div>
Two Scoops of Django 1.82015-07-14T09:32:00+02:002015-07-14T09:32:00+02:00SecNottag:secnot.com,2015-07-14:/django-two-scoops-1.8.html<p class="first last">El método mas sencillo de aceptar y validar DNI en un formulario Django</p>
<a class="reference external image-reference" href="http://twoscoopspress.org/products/two-scoops-of-django-1-8"><img alt="Portada del libro Two Scoops of Django 1.8" class="align-center" src="https://secnot.com/images/django-two-scoops-1.8/two-scoops-1.8.png" /></a>
<div class="line-block">
<div class="line"><br /></div>
</div>
<p>Se acaba de publicar la nueva entrega de
<a class="reference external" href="http://twoscoopspress.org/products/two-scoops-of-django-1-8">Two Scoops of Django</a>
la guía definitiva de buenas practicas para Django 1.8, tengo la versión anterior
y he comprado esta. No es un libro para principiantes, pero se <strong>lo recomiendo a todos
los desarrolladores experimentados</strong> que seguro que encuentran algún truco o
consejo útil. Además la versión de Django 1.8 tiene la particularidad de ser
“Long-Term Support” (LTS) por lo que tiene soporte de 3 años, así que el libro va
a estar actualizado durante más tiempo que versiones anteriores.</p>
<p>Por ahora sólo está disponible en inglés y lo puedes
<a class="reference external" href="http://www.amazon.es/gp/product/0981467342/ref=as_li_tf_tl?ie=UTF8&camp=3626&creative=24790&creativeASIN=0981467342&linkCode=as2&tag=sec0ff-21">comprar en Amazon</a></p>
Vista para la descarga de archivos generados dinámicamente2015-07-12T20:35:00+02:002015-07-12T20:35:00+02:00SecNottag:secnot.com,2015-07-12:/django-download-dynamically-generated-file.html<p class="first last">Vista para la descarga de archivos generados dinámicamente.</p>
<p>Hoy he estado programando en Django una vista para la descarga de informes, en
la que los usuarios registrados de una página pueden pedir un informe de la actividad
para un determinado año. La particularidad es que esos informes se generan de forma
dinámica en el momento de la petición, nada complicado pero antes de empezar a
programar he buscado que paquetes había disponibles, y me he encontrado
<a class="reference external" href="http://django-downloadview.readthedocs.org/en/1.7/">django-downloadview</a>.
Con eso y un poco de código extra para comprobar que el usuario está autenticado
he solucionado el problema. Algunas veces <strong>CBV</strong> parecen magia.</p>
<div class="highlight"><pre><span></span><span class="c1">#views.py</span>
<span class="kn">from</span> <span class="nn">django.shortcuts</span> <span class="kn">import</span> <span class="n">render</span>
<span class="kn">from</span> <span class="nn">django.core.files.base</span> <span class="kn">import</span> <span class="n">ContentFile</span>
<span class="kn">from</span> <span class="nn">django_downloadview</span> <span class="kn">import</span> <span class="n">VirtualDownloadView</span>
<span class="kn">from</span> <span class="nn">django.http</span> <span class="kn">import</span> <span class="n">HttpResponseForbidden</span><span class="p">,</span> <span class="n">HttpResponseServerError</span>
<span class="k">class</span> <span class="nc">LoginRequiredMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">():</span>
<span class="k">return</span> <span class="n">HttpResponseForbidden</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">DownloadReportView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">VirtualDownloadView</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">generar_report</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">year</span><span class="p">):</span>
<span class="c1"># Generar contenido del informe para el usuario y fecha</span>
<span class="k">return</span> <span class="s2">"Report content for </span><span class="si">{}</span><span class="s2"> </span><span class="si">{}</span><span class="s2">"</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="p">,</span> <span class="n">year</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_file</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="c1"># Metodo de VirtualDownloadView que devuelve el archivo virtual</span>
<span class="n">report_name</span> <span class="o">=</span> <span class="s1">'report.txt'</span>
<span class="k">return</span> <span class="n">ContentFile</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">report_content</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">report_name</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">report_year</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'year'</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="c1"># El contenido del informe se genera aquí en lugar de en get_file, para</span>
<span class="c1"># simplificar la</span>
<span class="k">try</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">report_content</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">generate_report</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="p">,</span> <span class="n">report_year</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="k">return</span> <span class="n">HttpResponseServerError</span><span class="p">(</span><span class="s2">"There was an error while generating the report"</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">DownloadReportView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</pre></div>
<p>Y el archivo <strong>urls.py</strong> para quien le pueda interesar:</p>
<div class="highlight"><pre><span></span><span class="c1">#urls.py</span>
<span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">.views</span> <span class="kn">import</span> <span class="n">DownloadReportView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">url</span><span class="p">(</span> <span class="c1"># Handle report downloads</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^download_report/(?P<year>[0-9]</span><span class="si">{4}</span><span class="s1">)$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">DownloadReportView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s1">'download_user_report'</span><span class="p">),</span>
<span class="p">]</span>
</pre></div>
Crea un servicio de Hosting VPS con Django I2015-07-08T23:33:00+02:002015-07-08T23:33:00+02:00SecNottag:secnot.com,2015-07-08:/django-vps-hosting-digitalocean-1.html<p class="first last">Como crea tu propio servicio de Hosting VPS usando Django y DigitalOcean.</p>
<p>Este es el primer artículo de una series en la que describo como usar
<strong>Django</strong> en conjunción con <strong>DigitalOcean</strong> para crear tu propio proveedor de
de servidores VPS. Algo que hace años hubiera sido impensable sin un equipo de
administradores y programadores, y que hoy puede implementarse con unos pocos
miles de lineas de código python.</p>
<p>Este artículo es una introducción al API de <strong>DigitalOcean</strong> y a su libreria de python,
la cual usaré en futuros artículos para implementar el servicio.</p>
<div class="section" id="api-de-digitalocean">
<h2>API de DigitalOcean</h2>
<p>El <a class="reference external" href="https://developers.digitalocean.com/">API de Digitalocean</a> actualmente
se encuentre en su segunda versión y es muy extenso, no solo permite crear, destruir,
y manejar <strong>Droplets</strong> <em>(nombre que usa para sus VPS)</em>, sino que permite controlar
todos los aspectos de sus servicios, desde la gestión de DNS, hasta la creación
backups, o gestión imágenes. Puede acceder a la documentación del API
<a class="reference external" href="https://developers.digitalocean.com/documentation/v2/">aquí</a>
pero por suerte ya existe una libreria para python llamada
<a class="reference external" href="https://github.com/koalalorenzo/python-digitalocean">python-digitalocean</a>
que simplifica enormemente el proceso, <strong>crear un Droplet</strong> puede ser tan sencillo
como:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">digitalocean</span>
<span class="n">droplet</span> <span class="o">=</span> <span class="n">digitalocean</span><span class="o">.</span><span class="n">Droplet</span><span class="p">(</span><span class="n">token</span><span class="o">=</span><span class="s2">"digitalocean-personal-access-token"</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'servidor.midominio.com'</span><span class="p">,</span>
<span class="n">region</span><span class="o">=</span><span class="s1">'ams2'</span><span class="p">,</span> <span class="c1"># Amsterdam</span>
<span class="n">image</span><span class="o">=</span><span class="s1">'ubuntu-14-04-x64'</span><span class="p">,</span> <span class="c1"># Ubuntu 14.04 x64</span>
<span class="n">size_slug</span><span class="o">=</span><span class="s1">'512mb'</span><span class="p">)</span>
<span class="n">droplet</span><span class="o">.</span><span class="n">create</span><span class="p">()</span>
<span class="n">droplet_id</span> <span class="o">=</span> <span class="n">droplet</span><span class="o">.</span><span class="n">id</span>
</pre></div>
<p>El identificador devuelto durante la creación del <em>Droplet</em> luego puede ser
usado para manejarlo, en el siguiente ejemplo se reinicia, apaga y después
elimina:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">digitalocean</span>
<span class="n">manager</span> <span class="o">=</span> <span class="n">digitalocean</span><span class="o">.</span><span class="n">Manager</span><span class="p">(</span><span class="n">token</span><span class="o">=</span><span class="s2">"digitalocean-personal-access-token"</span><span class="p">)</span>
<span class="n">droplet</span> <span class="o">=</span> <span class="n">manager</span><span class="o">.</span><span class="n">get_droplet</span><span class="p">(</span><span class="n">droplet_id</span><span class="p">)</span>
<span class="n">droplet</span><span class="o">.</span><span class="n">reboot</span><span class="p">()</span>
<span class="n">droplet</span><span class="o">.</span><span class="n">power_off</span><span class="p">()</span>
<span class="n">droplet</span><span class="o">.</span><span class="n">destroy</span><span class="p">()</span>
</pre></div>
<p>La librería también permite redirigir dominios gestionados desde la <strong>DNS</strong> de
<strong>DigitalOcean</strong>, por ejemplo si tienes la dirección IP y el <em>hostname</em> de un
<em>Droplet</em> puedes <strong>redirigir un subdominio</strong>:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">digitalocean</span>
<span class="kn">import</span> <span class="nn">tldextract</span>
<span class="n">droplet_ip</span> <span class="o">=</span> <span class="s2">"83.54.134.34"</span>
<span class="n">droplet_hostname</span> <span class="o">=</span> <span class="s2">"servidor.midominio.com"</span>
<span class="c1"># Crear subdominio usando el hostname del servidor</span>
<span class="n">d_subdomain</span><span class="p">,</span> <span class="n">d_domain</span><span class="p">,</span> <span class="n">d_suffix</span> <span class="o">=</span> <span class="n">tldextract</span><span class="o">.</span><span class="n">extract</span><span class="p">(</span><span class="n">droplet_hostname</span><span class="p">)</span>
<span class="n">domain</span> <span class="o">=</span> <span class="n">digitalocean</span><span class="o">.</span><span class="n">Domain</span><span class="p">(</span><span class="n">token</span><span class="o">=</span><span class="s2">"digitalocean-personal-access-token"</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="n">d_domain</span><span class="o">+</span><span class="s2">"."</span><span class="o">+</span><span class="n">d_suffix</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">domain</span><span class="o">.</span><span class="n">create_new_domain_record</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s2">"A"</span><span class="p">,</span> <span class="n">name</span><span class="o">=</span><span class="n">d_subdomain</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">droplet_ip</span><span class="p">)</span>
<span class="n">domain_record_id</span> <span class="o">=</span> <span class="n">result</span><span class="p">[</span><span class="s1">'domain_record'</span><span class="p">][</span><span class="s1">'id'</span><span class="p">]</span>
<span class="c1"># Eliminar registro creado</span>
<span class="n">record</span> <span class="o">=</span> <span class="n">digitalocean</span><span class="o">.</span><span class="n">Record</span><span class="p">(</span>
<span class="nb">id</span><span class="o">=</span><span class="n">domain_record_id</span><span class="p">,</span>
<span class="n">domain_name</span><span class="o">=</span><span class="n">d_domain</span><span class="o">+</span><span class="s2">"."</span><span class="o">+</span><span class="n">d_suffix</span><span class="p">,</span>
<span class="n">token</span><span class="o">=</span><span class="s2">"digitalocean-personal-access-token"</span><span class="p">)</span>
<span class="n">record</span><span class="o">.</span><span class="n">destroy</span><span class="p">()</span>
</pre></div>
<p>Y con esto tenemos los fundamentos para manejar <em>Droplets</em>, pero antes de poder entregarlo
al usuario es necesario configurarlo/customizarlo al gusto del usuario.</p>
</div>
<div class="section" id="metadata-y-cloudinit">
<h2>Metadata y Cloudinit</h2>
<p>DigitalOcean incluye un <a class="reference external" href="https://developers.digitalocean.com/documentation/metadata/">API de Metadatos</a>,
que permite a los <em>Droplets</em> acceder a sus propios datos, suministrar <em>datos de usuario</em>
durante su creación, y procesar esos datos usando <a class="reference external" href="https://cloudinit.readthedocs.org/en/latest/">CloudInit</a>
si es que contienen alguno de los formatos soportados. En el siguiente ejemplo se envía una
cadena que contiene dos variables:</p>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">digitalocean</span>
<span class="n">data</span> <span class="o">=</span> <span class="s1">'USUARIO="admin"</span><span class="se">\n</span><span class="s1">CLAVE="secreto"'</span>
<span class="n">droplet</span> <span class="o">=</span> <span class="n">digitalocean</span><span class="o">.</span><span class="n">Droplet</span><span class="p">(</span><span class="n">token</span><span class="o">=</span><span class="n">settings</span><span class="o">.</span><span class="n">DIGITALOCEAN_SECRET_TOKEN</span><span class="p">,</span>
<span class="n">name</span><span class="o">=</span><span class="s1">'servidor.midominio.com'</span><span class="p">,</span>
<span class="n">region</span><span class="o">=</span><span class="s1">'ams2'</span><span class="p">,</span> <span class="c1"># Amsterdam</span>
<span class="n">image</span><span class="o">=</span><span class="s1">'ubuntu-14-04-x64'</span><span class="p">,</span>
<span class="n">size_slug</span><span class="o">=</span><span class="s1">'512mb'</span><span class="p">,</span>
<span class="n">user_data</span><span class="o">=</span><span class="n">data</span><span class="p">)</span>
<span class="n">droplet</span><span class="o">.</span><span class="n">create</span><span class="p">()</span>
<span class="n">droplet_id</span> <span class="o">=</span> <span class="n">droplet</span><span class="o">.</span><span class="n">id</span>
</pre></div>
<p>Desde el <em>droplet</em> se puede acceder a los datos suministrados:</p>
<div class="highlight"><pre><span></span>$ curl -sS http://169.254.169.254/metadata/v1/user-data
<span class="nv">USUARIO</span><span class="o">=</span><span class="s2">"admin"</span>
<span class="nv">CLAVE</span><span class="o">=</span><span class="s2">"secreto"</span>
</pre></div>
<p><strong>CloudInit</strong> como su nombre indica es una herramienta para inicializar servidores
en la nube, la primera vez que arranca el servidor extrae los datos de configuración,
y si contienen alguno de los formatos soportados los procesa. Uno de esos formatos es
<strong>shell script</strong>, por ejemplo podemos enviar el siguiente script para crear un archivo
de swap de 2GB:</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
fallocate -l 2G /swapfile
chmod <span class="m">600</span> /swapfile
mkswap /swapfile
<span class="nb">echo</span> <span class="s1">'/swapfile none swap sw 0 0'</span> >>/etc/fstab
</pre></div>
<p><strong>Cloud Config</strong> es otro formato muy versátil, que permite realizar la mayoría de las
tareas más comunes de configuración de forma muy sencilla, y ademas proporciona
funcionalidad extra con su sistema de módulos, en el siguiente ejemplo se añade los
mismos 2GB de swap:</p>
<div class="highlight"><pre><span></span><span class="c1">#cloud-config</span>
<span class="nt">swap</span><span class="p">:</span>
<span class="nt">filename</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">/swapfile</span>
<span class="nt">size</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">2000000000</span>
</pre></div>
<p>Por último el formato <strong>Include File</strong> consiste en una lista de <strong>URLS</strong> cuyo
contenido es descargado y procesado usando las mismas reglas que si fueran los
datos proporcionados con user-data, por ejemplo:</p>
<div class="highlight"><pre><span></span><span class="c1">#include</span>
<span class="l l-Scalar l-Scalar-Plain">https://myvpsservice.com/scripts/swap-2GB</span>
<span class="l l-Scalar l-Scalar-Plain">https://myvpsservice.com/scripts/install-apache</span>
<span class="l l-Scalar l-Scalar-Plain">https://myvpsservice.com/scripts/add-keys</span>
</pre></div>
<p>Seguro que puedes ver el potencial de éste formato para esta aplicación. Si te
interesa conocer el resto de los formatos y módulos disponibles te recomiendo
que heches un vistazo a la
<a class="reference external" href="http://cloudinit.readthedocs.org/en/latest/topics/format.html">documentación</a>
de cloud-init.</p>
<p>En la segunda parte de esta serie profundizare más en como implementar un sistema
de configuración de VPS con Django.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="https://developers.digitalocean.com/documentation/v2/">DigitalOcean Api Documentation</a></li>
<li><a class="reference external" href="https://www.digitalocean.com/community/tutorials/an-introduction-to-droplet-metadata">Introduction to Droplet Metadata</a></li>
<li><a class="reference external" href="https://developers.digitalocean.com/documentation/metadata/">DigitalOceant Metadata Documentation</a></li>
<li><a class="reference external" href="https://www.digitalocean.com/community/tutorials/how-to-use-cloud-config-for-your-initial-server-setup">How to use CloudInit</a></li>
<li><a class="reference external" href="http://cloudinit.readthedocs.org/en">CloudInit Documentation</a></li>
</ul>
</div>
Funcionamiento de JPEG explicado2015-06-27T15:04:00+02:002015-06-27T15:04:00+02:00SecNottag:secnot.com,2015-06-27:/video-jpeg-algorithm.html<p class="first last">Videos explicando el funcionamiento del formato JPEG (Ingles)</p>
<p>Los videos de <a class="reference external" href="https://www.youtube.com/user/Computerphile/featured">Computerphile</a>
siempre son interesantes, pero la última serie de tres videos explicando el funcionamiento
del algoritmo de compresión usado en jpeg, me ha parecido especialmente inspirada:</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="youtube"><iframe src="https://www.youtube.com/embed/n_uNPbdenRs" width="640" height="360" allowfullscreen seamless frameBorder="0"></iframe></div><div class="line-block">
<div class="line"><br /></div>
</div>
<div class="youtube"><iframe src="https://www.youtube.com/embed/Q2aEzeMDHMA" width="640" height="360" allowfullscreen seamless frameBorder="0"></iframe></div><div class="line-block">
<div class="line"><br /></div>
</div>
<div class="youtube"><iframe src="https://www.youtube.com/embed/yBX8GFqt6GA" width="640" height="360" allowfullscreen seamless frameBorder="0"></iframe></div><div class="line-block">
<div class="line"><br /></div>
</div>
<p>Si te han gustado los videos, puedes seguir profundizando con éste
<a class="reference external" href="http://www.ams.org/samplings/feature-column/fcarc-image-compression">artículo</a>
<em>(tambíen en Inglés)</em>.</p>
DNIField para Django2015-06-24T12:20:00+02:002015-06-24T12:20:00+02:00SecNottag:secnot.com,2015-06-24:/django-dnifield.html<p class="first last">El método mas sencillo de aceptar y validar DNI en un formulario Django</p>
<p>Si vas a crear un formulario donde se pueda introducir un DNI/NIF no necesitas
crear tu propio DNIField o programar un validador, ya existe en un paquete llamado
<strong>django-localflavor</strong> que agrupa ese tipo de campos para los distintos países.</p>
<p>Por ejemplo para España incluye:</p>
<ul class="simple">
<li><strong>ESPhoneNumberField</strong> - Números de teléfonos fijos y móviles.</li>
<li><strong>ESIdentityCardNumberField</strong> - Numero de identificación NIF/CIF/NIE.</li>
<li><strong>ESCCCField</strong> - Código de cuenta de cliente en formato EEEE-OOOO-CC-AAAAAAAAAA</li>
<li><strong>ESProvinceSelect</strong> - Selección de provincia.</li>
</ul>
<div class="section" id="instalacion">
<h2>Instalación</h2>
<p>Como siempre pip es la mejor opción para instalar el paquete:</p>
<div class="highlight"><pre><span></span>$ pip install django-localflavor
</pre></div>
<p>Luego añade 'localflavor' a <strong>INSTALLED_APPS</strong> en settings.conf:</p>
<div class="highlight"><pre><span></span><span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">(</span>
<span class="c1"># ...</span>
<span class="s1">'localflavor'</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
<div class="section" id="uso">
<h2>Uso</h2>
<p>Un ejemplo de como crear un formulario sencillo:</p>
<div class="highlight"><pre><span></span><span class="c1">#forms.py</span>
<span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span>
<span class="kn">from</span> <span class="nn">localflavor.es.forms</span> <span class="kn">import</span> <span class="n">ESIdentityCardNumberField</span><span class="p">,</span> <span class="n">ESPhoneNumberField</span><span class="p">,</span> <span class="n">ESProvinceField</span>
<span class="k">class</span> <span class="nc">ClienteForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span>
<span class="n">nombre</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">200</span><span class="p">)</span>
<span class="n">dni</span> <span class="o">=</span> <span class="n">ESIdentityCardNumberField</span><span class="p">(</span><span class="n">only_nif</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">telefono</span> <span class="o">=</span> <span class="n">ESPhoneNumberField</span><span class="p">()</span>
<span class="o">...</span>
</pre></div>
<p>Al campo dni se le ha añadido la opción <strong>only_nif=True</strong> para que sólo acepte <strong>NIF</strong> y
<strong>NIE</strong> <em>(Numero identificación extranjeros)</em>, sin ella también aceptaría códigos <strong>CIF</strong>.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://django-localflavor.readthedocs.org/en/latest/">Localflavour documentation</a></li>
<li><a class="reference external" href="http://django-localflavor.readthedocs.org/en/latest/localflavor/es/">Localflavour Spain (ES)</a></li>
</ul>
</div>
Inicializar la base de datos en Django2015-06-22T15:49:00+02:002015-06-22T15:49:00+02:00SecNottag:secnot.com,2015-06-22:/django-incializar-db.html<p class="first last">Como almacenar los datos de inicialización de la base de datos de una aplicación Django.</p>
<p>Algunas veces es necesario inicializar la base de datos antes de poder usar
una aplicación, si esta aplicación no va a reutilizarse no es ningún problema,
en cambio si es algo que planeas usar en varios proyectos, es mucho mas práctico
añadir los datos de inicialización en la misma aplicación y cargarlos usando
<strong>loaddata</strong>. Imaginemos una aplicación para la gestión de los productos de una
tienda, en la que tenemos los siguientes modelos:</p>
<div class="highlight"><pre><span></span><span class="c1">#tienda/models.py</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="k">class</span> <span class="nc">Categoria</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">nombre</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">80</span><span class="p">)</span>
<span class="n">slug</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">SlugField</span><span class="p">(</span><span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">db_index</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">descripcion</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">2000</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Producto</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">nombre</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">Charfield</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">200</span><span class="p">)</span>
<span class="n">descripcion</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">2000</span><span class="p">)</span>
<span class="n">precio</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DecimalField</span><span class="p">()</span>
<span class="n">categoria</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Categoria</span><span class="p">)</span>
</pre></div>
<p>Django permite crear un directorio llamado <strong>fixtures</strong> dentro de la aplicación donde
almacenar archivos de datos que luego pueden ser volcados a la base de datos para
inicializarla.</p>
<p>Estos archivos pueden crearse usado tres formatos <strong>JSON</strong>, <strong>YAML</strong>, <strong>XML</strong>, yo creo
que los dos primeros son la mejor opción, más fáciles de leer y modificar manualmente.
Veamos un ejemplo para <strong>JSON</strong>:</p>
<div class="highlight"><pre><span></span><span class="p">[</span>
<span class="p">{</span>
<span class="nt">"model"</span><span class="p">:</span> <span class="s2">"tienda.categoria"</span><span class="p">,</span>
<span class="nt">"fields"</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">"nombre"</span><span class="p">:</span> <span class="s2">"Ordenadores portatiles"</span><span class="p">,</span>
<span class="nt">"slug"</span><span class="p">:</span> <span class="s2">"portatiles"</span><span class="p">,</span>
<span class="nt">"descripcion"</span><span class="p">:</span> <span class="s2">"Ordenadores portatiles de 13 a 17 pulgadas"</span><span class="p">,</span>
<span class="p">},</span>
<span class="nt">"pk"</span><span class="p">:</span> <span class="mi">100000</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">"model"</span><span class="p">:</span> <span class="s2">"tienda.categoria"</span><span class="p">,</span>
<span class="nt">"fields"</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">"nombre"</span><span class="p">:</span> <span class="s2">"Tablets"</span><span class="p">,</span>
<span class="nt">"slug"</span><span class="p">:</span> <span class="s2">"tablets"</span><span class="p">,</span>
<span class="nt">"descripcion"</span><span class="p">:</span> <span class="s2">"Tablets android"</span><span class="p">,</span>
<span class="p">},</span>
<span class="nt">"pk"</span><span class="p">:</span> <span class="mi">100001</span>
<span class="p">},</span>
<span class="p">{</span>
<span class="nt">"model"</span><span class="p">:</span> <span class="s2">"tienda.producto"</span><span class="p">,</span>
<span class="nt">"fields"</span><span class="p">:</span> <span class="p">{</span>
<span class="nt">"nombre"</span><span class="p">:</span> <span class="s2">"ASUS MeMO Pad 10 ME103K 16GB"</span><span class="p">,</span>
<span class="nt">"precio"</span><span class="p">:</span> <span class="s2">"120.50"</span><span class="p">,</span>
<span class="nt">"descripcion"</span><span class="p">:</span> <span class="s2">"La ASUS MeMO Pad 10 se creó pensando en..."</span><span class="p">,</span>
<span class="nt">"categoria"</span><span class="p">:</span> <span class="mi">100001</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">]</span>
</pre></div>
<p>y los mismos datos en <strong>YAML</strong>:</p>
<div class="highlight"><pre><span></span><span class="p p-Indicator">-</span> <span class="nt">model</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">tienda.categoria</span>
<span class="nt">pk</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">100000</span>
<span class="nt">fields</span><span class="p">:</span>
<span class="nt">nombre</span><span class="p">:</span> <span class="s">"Ordenadores</span><span class="nv"> </span><span class="s">portatiles"</span>
<span class="nt">slug</span><span class="p">:</span> <span class="s">"portatiles"</span>
<span class="nt">descripcion</span><span class="p">:</span> <span class="s">"Ordenadores</span><span class="nv"> </span><span class="s">portatiles</span><span class="nv"> </span><span class="s">de</span><span class="nv"> </span><span class="s">13</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">17</span><span class="nv"> </span><span class="s">pulgadas"</span>
<span class="nt">-model</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">tienda.categoria</span>
<span class="nt">pk</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">100001</span>
<span class="nt">fields</span><span class="p">:</span>
<span class="nt">nombre</span><span class="p">:</span> <span class="s">"Tablets"</span>
<span class="nt">slug</span><span class="p">:</span> <span class="s">"tablets"</span>
<span class="nt">descripcion</span><span class="p">:</span> <span class="s">"Tablets</span><span class="nv"> </span><span class="s">Android"</span>
<span class="p p-Indicator">-</span> <span class="nt">model</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">tienda.producto</span>
<span class="nt">pk</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">100000</span>
<span class="nt">fields</span><span class="p">:</span>
<span class="nt">nombre</span><span class="p">:</span> <span class="s">"ASUS</span><span class="nv"> </span><span class="s">MeMO</span><span class="nv"> </span><span class="s">Pad</span><span class="nv"> </span><span class="s">10</span><span class="nv"> </span><span class="s">ME103K</span><span class="nv"> </span><span class="s">16GB"</span>
<span class="nt">precio</span><span class="p">:</span> <span class="s">"120.50"</span>
<span class="nt">descripcion</span><span class="p">:</span> <span class="s">"La</span><span class="nv"> </span><span class="s">ASUS</span><span class="nv"> </span><span class="s">MeMO</span><span class="nv"> </span><span class="s">Pad</span><span class="nv"> </span><span class="s">10</span><span class="nv"> </span><span class="s">se</span><span class="nv"> </span><span class="s">creó</span><span class="nv"> </span><span class="s">pensando</span><span class="nv"> </span><span class="s">en..."</span>
<span class="nt">categoria</span><span class="p">:</span> <span class="l l-Scalar l-Scalar-Plain">100001</span>
</pre></div>
<p>Si tienes el archivo de inicialización en la ruta <strong>tienda/fixtures/categorias.json</strong>, se
puede cargar los datos con:</p>
<div class="highlight"><pre><span></span>$ python manage.py loaddata categorias
</pre></div>
<p>Por último aunque es posible crear y editar manualmente los archivos, Django proporciona
una herramienta para volcar la base de datos a un archivo usando cualquiera de los formatos
soportado, con lo que se puede usar el interfaz Admin para crear los datos y luego volcarlos
con:</p>
<div class="highlight"><pre><span></span>$ python manage.py dumpdata tienda --indent <span class="m">4</span> --format json --output tienda/fixtures/categorias.json
</pre></div>
<p>Y eso es todo por hoy.</p>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="https://docs.djangoproject.com/en/dev/howto/initial-data/">Django initial-data Howto</a></li>
<li><a class="reference external" href="https://docs.djangoproject.com/en/dev/ref/django-admin/#django-admin-dumpdata">Django dumpdata Referece</a></li>
</ul>
</div>
Crear servidores KVM en Ubuntu 14.042015-05-24T19:05:00+02:002015-05-24T19:05:00+02:00SecNottag:secnot.com,2015-05-24:/kvm-server-ubuntu.html<p class="first last">Crea un servidor virtual facilmente en Ubuntu usando KVM</p>
<p>Algunas veces necesito una maquina virtual para probar aplicaciones antes de
desplegarlas, o para trastear con alguna configuración, este tutorial describe
el método para crearlas más rápido y sencillo que he encontrado.</p>
<div class="section" id="instalacion">
<h2>Instalación</h2>
<p>En el huésped en el que se van a alojar las maquinas, son necesarios los siguientes
paquetes:</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install ubuntu-virt-server qemu-kvm libvirt-bin ubuntu-vm-builder bridge-utils
</pre></div>
<p>desde donde vayas gestionar las maquinas, es recomendable instalar:</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install virt-manager
</pre></div>
</div>
<div class="section" id="configuracion-de-red">
<h2>Configuracion de red</h2>
<p>El siguiente paso es modificar la configuración de red del anfitrión, yo prefiero
usar <em>bridging</em> de manera que las maquinas virtuales tengan asignadas direcciones
de red validas. El funcionamiento es ingenioso, se crea un <em>switch</em> virtual
usando la tarjeta de red física, al que los huéspedes se conectan como si se
tratara de una conexión real.</p>
<p>Solo es necesario modificar en archivo <strong>/etc/network/interfaces</strong>, por ejemplo
si este es el archivo original:</p>
<div class="highlight"><pre><span></span><span class="c1">#/etc/network/interfaces</span>
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address <span class="m">192</span>.168.0.42
network <span class="m">192</span>.168.0.0
netmask <span class="m">255</span>.255.255.0
broadcast <span class="m">192</span>.168.0.255
gateway <span class="m">192</span>.168.0.1
dns-nameserver <span class="m">80</span>.53.60.25
</pre></div>
<p>Debemos convertir el interfaz <strong>eth0</strong> en <strong>br0</strong> y añadir las opciones:</p>
<div class="highlight"><pre><span></span><span class="c1">#/etc/network/interfaces</span>
auto lo
iface lo inet loopback
auto br0
iface br0 inet static
address <span class="m">192</span>.168.0.42
network <span class="m">192</span>.168.0.0
netmask <span class="m">255</span>.255.255.0
broadcast <span class="m">192</span>.168.0.255
gateway <span class="m">192</span>.168.0.1
dns-nameserver <span class="m">80</span>.53.60.25
bridge_ports eth0
bridge_stp off
bridge_fd <span class="m">0</span>
bridge_maxwait <span class="m">4</span>
</pre></div>
<p>Tras esto reinicia el sistema o el servicio de red.</p>
<div class="highlight"><pre><span></span>$ /etc/init.d/networking restart
$ reboot
</pre></div>
</div>
<div class="section" id="configuracion-del-sistema">
<h2>Configuración del sistema</h2>
<p>Añade un usuario al grupo de libvirtd y kvm para que pueda manipulas las maquinas,
sin necesidad de ser root:</p>
<div class="highlight"><pre><span></span>$ sudo adduser <span class="sb">`</span>id -un<span class="sb">`</span> libvirtd
$ sudo adduser <span class="sb">`</span>id -un<span class="sb">`</span> kvm
</pre></div>
<p>Crea un directorio donde almacenar las imágenes KVM, por ejemplo:</p>
<div class="highlight"><pre><span></span>$ mkdir /home/kvm-images
$ chown libvirt-qemu:libvirtd /home/kvn-images
</pre></div>
<p>Adicionalmente puedes crea un par de claves ssh para el usuario, de forma que
no tengas que introducir la clave cada vez que administras remotamente la máquina.
En <a class="reference external" href="https://secnot.com/seguridad-ssh-1.html">este post</a> explico como se hace.</p>
</div>
<div class="section" id="crear-la-maquina-virtual">
<h2>Crear la máquina virtual</h2>
<p>Con todo configurado ya estamos listos para crear la máquina con el comando
<strong>ubuntu-vm-builder</strong>:</p>
<div class="highlight"><pre><span></span>$ sudo ubuntu-vm-builder kvm trusty <span class="se">\</span>
--domain servidor <span class="se">\</span>
--hostname kvm1 <span class="se">\</span>
--arch amd64 <span class="se">\</span>
--mem <span class="m">2048</span> <span class="se">\</span>
--cpus <span class="m">1</span> <span class="se">\</span>
--rootsize <span class="m">20000</span> <span class="se">\</span>
--swapsize <span class="m">2048</span> <span class="se">\</span>
--destdir /home/kvm-images/kvm2 <span class="se">\</span>
--user secnot <span class="se">\</span>
--pass secreto <span class="se">\</span>
--bridge br0 <span class="se">\</span>
--ip <span class="m">192</span>.168.0.201 <span class="se">\</span>
--mask <span class="m">255</span>.255.255.0 <span class="se">\</span>
--net <span class="m">192</span>.168.0.0 <span class="se">\</span>
--bcast <span class="m">192</span>.168.0.255 <span class="se">\</span>
--gw <span class="m">192</span>.168.0.1 <span class="se">\</span>
--dns <span class="m">80</span>.53.60.25 <span class="se">\</span>
--components main,universe <span class="se">\</span>
--addpkg acpid <span class="se">\</span>
--addpkg openssh-server <span class="se">\</span>
--addpkg linux-image-generic <span class="se">\</span>
--addpkg unattended-upgrades <span class="se">\</span>
--libvirt qemu:///system <span class="p">;</span>
</pre></div>
<p>Aunque todos esos parámetros parecen muy complicados, sólo la primera linea
es necesaria, las demás permiten especificar la configuración para el huésped:</p>
<ul class="simple">
<li><strong>destdir</strong> - Directorio en el que quieres que se cree la imagen.</li>
<li><strong>domain, hostname</strong> - Dominio y nombre de host del huésped.</li>
<li><strong>arch</strong> - Arquitectura de la maquina a instalar.</li>
<li><strong>mem, cpus</strong> - Cuanta memoria y CPUs son asignadas al huésped.</li>
<li><strong>rootsize, swapsize</strong> - Tamaño del disco raíz en MB (por defecto 4000MB),
y de la partición de intercambio.</li>
<li><strong>user, pass</strong> - Nombre de usuario y clave para la primera cuenta de usuario
en el huésped.</li>
<li><strong>bridge</strong> - Indica a que bridge conectar el interfaz de red del huésped.</li>
<li><strong>ip, mask, bcast, gw, dns</strong> - Configuración de red para el huésped.</li>
<li><strong>mirror, components</strong> - Indica al huésped de que repositorio descargar los
paquetes, útil para acelerar la instalacion si tienes copias locales.</li>
<li><strong>addpkg</strong> - Paquetes a instalar en el huesped durante su creación. Algunos
paquetes son imprescindibles, <em>acpid</em> para poder apagar la maquina,
<em>openssh-server</em> para poder conectarse remotamente, y <em>linux-image-generic</em>
para evitar el error "This kernel does not support a non-PAE CPU"</li>
<li><strong>libvirt</strong> - Indica al instalador en que host debe instalar la maquina.</li>
<li><strong>ssh-key</strong> - Añade la clave pública suministrada (ruta absoluta) a la
lista de claves autorizadas del root.</li>
</ul>
<p>Podemos comprobar que está funcionando correctamente con <strong>virsh</strong>:</p>
<div class="highlight"><pre><span></span>$ virsh list --all
Id Nombre Estado
----------------------------------------------------
<span class="m">1</span> kvm1 ejecutando
</pre></div>
<p>Si no fue arrancado automáticamente, puedes hacerlo a mano con:</p>
<div class="highlight"><pre><span></span>$ virsh start kvm1
Se ha iniciado el dominio kvm1
</pre></div>
<p>y detenerlo con:</p>
<div class="highlight"><pre><span></span>$ virsh shutdown kvm1
El dominio kvm1 está siendo apagado
</pre></div>
</div>
<div class="section" id="gestion-remota">
<h2>Gestión remota</h2>
<p>Si además quieres gestionar de forma remota todos los huéspedes KVM, instala <strong>virt-manager</strong>,
y añade la dirección ip del anfitrión con <strong>Archivo>Añadir conexión...</strong> usando como
nombre de usuario la cuenta que añadiste a los grupos de kvm. El resultado debería ser
similar a esto:</p>
<img alt="Ventana principal de virt-manager" src="https://secnot.com/images/kvm-server-ubuntu/virt-manager-main.png" />
<p>Si haces doble click en cualquiera de las máquinas, puedes abrir una consola, o la
página de detalles de configuración seleccionando <strong>vista</strong> en la barra de tareas:</p>
<img alt="Ventana detalles virt-manager" src="https://secnot.com/images/kvm-server-ubuntu/virt-manager-details.png" />
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://www.linux-kvm.org/page/Main_Page">Pagina oficial de KVM</a></li>
<li><a class="reference external" href="http://manpages.ubuntu.com/manpages/hardy/man1/ubuntu-vm-builder.1.html">Manual ubuntu-vm-builder</a></li>
<li><a class="reference external" href="https://www.howtoforge.com/virtualization-with-kvm-on-ubuntu-12.04-lts">Virtualization With KVM On Ubuntu 12.04 LTS</a></li>
</ul>
</div>
Scripts que se ejecutan sólo una vez2015-04-24T22:02:00+02:002015-04-24T22:02:00+02:00SecNottag:secnot.com,2015-04-24:/scripts-ejecutan-una-vez.html<p class="first last">Como crear scripts que se ejecuten una única vez.</p>
<p>A veces es necesario un script shell que se ejecute una sola vez, por ejemplo
cuando estas trabajando en una imagen de una maquina virtual o una distribución,
y necesitas que se ejecute un script la primera vez que arranca el sistema. Estos
scripts son faciles de escribir pero hay que tener cuidado con ciertos detalles
que pueden dar problemas.</p>
<p>La primera aproximación es eliminar el script tras se ejecutado.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1"># Tus comandos aqui</span>
<span class="c1"># Elimina el script antes de salir</span>
rm <span class="nv">$0</span>
</pre></div>
<p>El problema de éste sistema es que <strong>$0</strong> no devuelve la ruta del script sino que
devuelve el nombre usado para su invocación, si se usó ruta absoluta no hay problema,
pero si se usó relativa puede darse el caso que si se ha cambiado el directorio de
trabajo durante su ejecución, no borrara el script o incluso puede borrar un
archivo con el mismo nombre en otro directorio.</p>
<p>La solución es obtener la ruta absoluta del script antes al inicio, y usarla para
borrar el archivo al finalizar.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1"># Obtener ruta absoluta</span>
<span class="nv">RUTA_ABSOLUTA</span><span class="o">=</span><span class="k">$(</span><span class="nb">cd</span> <span class="sb">`</span>dirname <span class="s2">"</span><span class="si">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="si">}</span><span class="s2">"</span><span class="sb">`</span> <span class="o">&&</span> <span class="nb">pwd</span><span class="k">)</span>/<span class="sb">`</span>basename <span class="s2">"</span><span class="si">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="si">}</span><span class="s2">"</span><span class="sb">`</span>
<span class="c1"># Tus comandos aqui</span>
<span class="c1"># Elimina el script</span>
rm <span class="nv">$RUTA_ABSOLUTA</span>
</pre></div>
<p>Esto es suficiente en la mayoría de los casos, pero en algunas situaciones no es
posible eliminar el script porque puede ser necesario posteriormente. En estos casos
se usa un <em>flag</em> para determinar si el script ya se ha ejecutado.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="nv">FLAG</span><span class="o">=</span><span class="s2">"/var/log/archivo-flag.log"</span>
<span class="k">if</span> <span class="o">[</span> ! -f <span class="nv">$FLAG</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
<span class="nb">echo</span> <span class="s2">"Primera ejecucion del script"</span>
<span class="c1"># Crear flag para recordar que se ejecuto</span>
touch <span class="nv">$FLAG</span>
<span class="k">else</span>
<span class="nb">echo</span> <span class="s2">"El script solo se puede ejecutar una vez"</span>
<span class="nb">exit</span> <span class="m">0</span>
<span class="k">fi</span>
<span class="c1"># Tus comandos aqui</span>
</pre></div>
<p>Aquí se ha usado un archivo como <em>flag</em> que es creado en la primera ejecución, cuando
se intenta ejecutar el script posteriormente se detecta la existencia del archivo y
no se continua con la ejecución. Si quisieras ejecutarlo una vez mas, no hay mas que
eliminar el archivo <em>flag</em>.</p>
<p>Este sistema tampoco es perfecto, si por algún error se borra el archivo <em>flag</em> el
script se ejecutaría de nuevo, por lo que hay que tener cuidado con donde y con que
permisos se crea.</p>
<p>Y eso es todo, elige el método que mas te guste.</p>
Crea tu tienda online con Django y Paypal REST I2015-04-17T11:34:00+02:002015-04-17T11:34:00+02:00SecNottag:secnot.com,2015-04-17:/django-shop-paypal-rest-1.html<p class="first last">Crea tu tienda online con Django y acepta pago con Paypal REST API.</p>
<p>He leído infinidad de tutoriales sobre PayPal REST API, desde los más técnicos a los más prácticos,
y en todos ellos lo que he echado de menos es que profundizaran un poco más en como integrar paypal
en una aplicación real.
Este tutorial es la primera parte de una serie, que pretende llenar ese hueco, y esta pensado como
un complemento a esos otros tutoriales, mostrando distintos ejemplos de uso de PayPal.</p>
<p>Antes de empezar recomiendo que te familiarizes con la biblioteca
<a class="reference external" href="https://github.com/paypal/PayPal-Python-SDK">Paypal Python SDK</a>, porque el propósito de éste
tutorial no es explicar su funcionamiento.</p>
<div class="section" id="la-tienda">
<h2>La tienda</h2>
<p>Para éste primer tutorial la aplicación que voy a usar como ejemplo es una tienda de libros, en
concreto una tienda de ebooks. Su funcionamiento es muy sencillo, cada libro tiene un botón de
compra que al ser pulsado redirige al cliente a la página de pago de paypal, cuando la transacción
se finaliza, el libro es enviado a su dirección de correo.</p>
<p>El proyecto esta compuesto por dos <em>"apps"</em> principales, <strong>libros</strong> y <strong>pagos</strong>. La primera
gestiona el contenido de la tienda, con cuatro modelos <strong>Autor</strong>, <strong>Genero</strong>, <strong>Editorial</strong>,
y <strong>Libro</strong>. Siendo este último el modelo más importante.</p>
<div class="highlight"><pre><span></span><span class="c1"># libros.models.py</span>
<span class="k">class</span> <span class="nc">Libro</span><span class="p">(</span><span class="n">PublicadoMixin</span><span class="p">,</span> <span class="n">ImagenMixin</span><span class="p">):</span>
<span class="sd">"""Novels are never displayed, and are used to link together</span>
<span class="sd"> all the editions, with the common information among them</span>
<span class="sd"> authors, genre..."""</span>
<span class="c1"># Informacion basica del libro</span>
<span class="n">titulo</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span> <span class="n">db_index</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">titulo_completo</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">300</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">numero_paginas</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">PositiveIntegerField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
<span class="n">resumen</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">2000</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">fecha_publicacion</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DateField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">idioma</span> <span class="o">=</span> <span class="n">LanguageField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="s1">'es'</span><span class="p">)</span>
<span class="c1"># Informacion sobre edicion y editorial</span>
<span class="n">numero_edicion</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">PositiveSmallIntegerField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">editorial</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Editorial</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># Algunos libros pueden tener multiples autores</span>
<span class="n">autores</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ManyToManyField</span><span class="p">(</span><span class="s1">'Autor'</span><span class="p">)</span>
<span class="c1"># Generos del libro</span>
<span class="n">generos</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ManyToManyField</span><span class="p">(</span><span class="s1">'Genero'</span><span class="p">)</span>
<span class="c1"># Ruta del ebook en los distintos formatos</span>
<span class="n">epub</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">FileField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="s1">'ebooks/epub/'</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">mobi</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">FileField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="s1">'ebooks/mobi/'</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># ISBN del libro.</span>
<span class="n">isbn</span> <span class="o">=</span> <span class="n">ISBNField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># Precio del libro</span>
<span class="n">precio</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DecimalField</span><span class="p">(</span><span class="n">max_digits</span><span class="o">=</span><span class="mi">6</span><span class="p">,</span> <span class="n">decimal_places</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">META</span><span class="p">:</span>
<span class="n">ordering</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'-fecha_publicacion'</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">get_absolute_url</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="n">reverse</span><span class="p">(</span><span class="s1">'libro-detail'</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">),])</span>
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">titulo</span>
</pre></div>
<p>La segunda <em>"app"</em> <strong>pagos</strong> se encarga de generar y aceptar los pagos por PayPal, y tiene
un único modelo que se usa para registrar la información de la transacción, en concepto
de que libro se realizaron, y la información del cliente proporcionada por PayPal.</p>
<div class="highlight"><pre><span></span><span class="c1"># pagos.models.py</span>
<span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">decimal</span> <span class="kn">import</span> <span class="n">Decimal</span>
<span class="kn">from</span> <span class="nn">libros.models</span> <span class="kn">import</span> <span class="n">Libro</span>
<span class="k">class</span> <span class="nc">PagoPaypalManager</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Manager</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">crear_pago</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">payment_id</span><span class="p">,</span> <span class="n">libro</span><span class="p">):</span>
<span class="n">pago</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">libro</span><span class="o">=</span><span class="n">libro</span><span class="p">,</span>
<span class="n">payment_id</span><span class="o">=</span><span class="n">payment_id</span><span class="p">,</span>
<span class="n">precio</span><span class="o">=</span><span class="n">libro</span><span class="o">.</span><span class="n">precio</span><span class="p">)</span>
<span class="k">return</span> <span class="n">pago</span>
<span class="k">class</span> <span class="nc">PagoPaypal</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="c1"># Foreign Key hacia el libro de este pago</span>
<span class="n">libro</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ForeignKey</span><span class="p">(</span><span class="n">Libro</span><span class="p">)</span>
<span class="c1"># Identificador de paypal para este pago</span>
<span class="n">payment_id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">db_index</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># Id unico asignado por paypal a cada usuario no cambia aunque</span>
<span class="c1"># la dirección de email lo haga.</span>
<span class="n">payer_id</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">db_index</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># Dirección de email del cliente proporcionada por paypal.</span>
<span class="n">payer_email</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">EmailField</span><span class="p">(</span><span class="n">blank</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="c1"># Guardamos una copia del precio de libro, porque puede variar en el tiempo</span>
<span class="n">precio</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">DecimalField</span><span class="p">(</span><span class="n">max_digits</span><span class="o">=</span><span class="mi">8</span><span class="p">,</span> <span class="n">decimal_places</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
<span class="n">default</span> <span class="o">=</span> <span class="n">Decimal</span><span class="p">(</span><span class="s1">'0.00'</span><span class="p">))</span>
<span class="n">pagado</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">BooleanField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">objects</span> <span class="o">=</span> <span class="n">PagoPaypalManager</span><span class="p">()</span>
</pre></div>
<p>Por último para introducir los datos de libros, editoriales, y demás información
se usa el interfaz de administración de Django.</p>
</div>
<div class="section" id="credenciales-de-paypal">
<h2>Credenciales de Paypal</h2>
<p>El primer paso para desarrollar una aplicación con PayPal es crear una cuenta,
habilitar REST Api en <a class="reference external" href="https://developer.paypal.com/">Paypal Developer</a>, y obtener
las credenciales y cuentas de desarrollo. Después solo hay que añadirlas en el
archivo <strong>settings.py</strong></p>
<div class="highlight"><pre><span></span><span class="n">PAYPAL_MODE</span><span class="o">=</span> <span class="s2">"sandbox"</span>
<span class="n">PAYPAL_CLIENT_ID</span> <span class="o">=</span> <span class="s2">"EBWKjlELKMYqRNQ6sYvFo64FtaRLRR5BdHEESmha49TM"</span><span class="p">,</span>
<span class="n">PAYPAL_CLIENT_SECRET</span> <span class="o">=</span> <span class="s2">"EO422dn3gQLgDbuwqTjzrFgFtaRLRR5BdHEESmha49TM"</span>
</pre></div>
</div>
<div class="section" id="paypal">
<h2>Paypal</h2>
<p>En las páginas con la información de cada uno de los libros, se encuentran los
respectivos botones de compra, con los cuales un cliente puede iniciar el proceso
de pago a traves de PayPal.</p>
<img alt="Imagén de la pagina de compra de un libro" src="https://secnot.com/images/django-shop-paypal-rest-1/libro-screenshot.jpg" />
<p>Veamos código del botón de compra en el template:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"{% url 'pago-paypal' libro.pk %}"</span> <span class="na">class</span><span class="o">=</span><span class="s">"btn btn-info btn-lg pull-right"</span><span class="p">></span>
Comprar {{libro.precio}}€
<span class="p"></</span><span class="nt">a</span><span class="p">></span>
</pre></div>
<p>El botón redirige al usuario a <strong>'pago-paypal'</strong> con el identificador del libro como
parámetro, ese alias corresponde con la vista <strong>pagos.views.PaypalView</strong>, donde
se encuentra la lógica para iniciar el proceso de pago del libro.</p>
<div class="highlight"><pre><span></span><span class="c1"># pagos.views.py</span>
<span class="k">class</span> <span class="nc">PaypalView</span><span class="p">(</span><span class="n">RedirectView</span><span class="p">):</span>
<span class="n">permanent</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">def</span> <span class="nf">_generar_lista_items</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">libro</span><span class="p">):</span>
<span class="sd">""" """</span>
<span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">({</span>
<span class="s2">"name"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">libro</span><span class="p">),</span>
<span class="s2">"sku"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">libro</span><span class="o">.</span><span class="n">id</span><span class="p">),</span>
<span class="s2">"price"</span><span class="p">:</span> <span class="p">(</span><span class="s1">'</span><span class="si">%.2f</span><span class="s1">'</span> <span class="o">%</span> <span class="n">libro</span><span class="o">.</span><span class="n">precio</span><span class="p">),</span>
<span class="s2">"currency"</span><span class="p">:</span> <span class="s2">"EUR"</span><span class="p">,</span>
<span class="s2">"quantity"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">return</span> <span class="n">items</span>
<span class="k">def</span> <span class="nf">_generar_peticion_pago_paypal</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">libro</span><span class="p">):</span>
<span class="sd">"""Crea el diccionario para genrar el pago paypal de libro"""</span>
<span class="n">peticion_pago</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"intent"</span><span class="p">:</span> <span class="s2">"sale"</span><span class="p">,</span>
<span class="s2">"payer"</span><span class="p">:</span> <span class="p">{</span><span class="s2">"payment_method"</span><span class="p">:</span> <span class="s2">"paypal"</span><span class="p">},</span>
<span class="s2">"redirect_urls"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"return_url"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">SITE_URL</span><span class="o">+</span><span class="n">reverse</span><span class="p">(</span><span class="s1">'aceptar-pago-paypal'</span><span class="p">),</span>
<span class="s2">"cancel_url"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">SITE_URL</span><span class="p">},</span>
<span class="c1"># Transaction -</span>
<span class="s2">"transactions"</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span>
<span class="c1"># ItemList</span>
<span class="s2">"item_list"</span><span class="p">:{</span>
<span class="s2">"items"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_generar_lista_items</span><span class="p">(</span><span class="n">libro</span><span class="p">)},</span>
<span class="c1"># Amount</span>
<span class="s2">"amount"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"total"</span><span class="p">:</span> <span class="p">(</span><span class="s1">'</span><span class="si">%.2f</span><span class="s1">'</span> <span class="o">%</span> <span class="n">libro</span><span class="o">.</span><span class="n">precio</span><span class="p">),</span>
<span class="s2">"currency"</span><span class="p">:</span> <span class="s1">'EUR'</span><span class="p">},</span>
<span class="c1">#Description</span>
<span class="s2">"description"</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">libro</span><span class="p">),}</span>
<span class="p">]}</span>
<span class="k">return</span> <span class="n">peticion_pago</span>
<span class="k">def</span> <span class="nf">_generar_pago_paypal</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">libro</span><span class="p">):</span>
<span class="sd">"""Genera un pago de paypal para libro"""</span>
<span class="n">paypalrestsdk</span><span class="o">.</span><span class="n">configure</span><span class="p">({</span>
<span class="s2">"mode"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">PAYPAL_MODE</span><span class="p">,</span>
<span class="s2">"client_id"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">PAYPAL_CLIENT_ID</span><span class="p">,</span>
<span class="s2">"client_secret"</span><span class="p">:</span><span class="n">settings</span><span class="o">.</span><span class="n">PAYPAL_CLIENT_SECRET</span><span class="p">,})</span>
<span class="n">pago_paypal</span> <span class="o">=</span> <span class="n">paypalrestsdk</span><span class="o">.</span><span class="n">Payment</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_generar_peticion_pago_paypal</span><span class="p">(</span><span class="n">libro</span><span class="p">))</span>
<span class="k">if</span> <span class="n">pago_paypal</span><span class="o">.</span><span class="n">create</span><span class="p">():</span>
<span class="k">for</span> <span class="n">link</span> <span class="ow">in</span> <span class="n">pago_paypal</span><span class="o">.</span><span class="n">links</span><span class="p">:</span>
<span class="k">if</span> <span class="n">link</span><span class="o">.</span><span class="n">method</span> <span class="o">==</span> <span class="s2">"REDIRECT"</span><span class="p">:</span>
<span class="n">url_pago</span> <span class="o">=</span> <span class="n">link</span><span class="o">.</span><span class="n">href</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">Exception</span><span class="p">(</span><span class="n">pago</span><span class="o">.</span><span class="n">error</span><span class="p">)</span>
<span class="k">return</span> <span class="n">url_pago</span><span class="p">,</span> <span class="n">pago_paypal</span>
<span class="k">def</span> <span class="nf">get_redirect_url</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="sd">"""Extrae el libro que el usuario quiere comprar, genera un pago de</span>
<span class="sd"> paypal por el precio del libro, y devuelve la direccion de pago que</span>
<span class="sd"> paypal generó"""</span>
<span class="n">libro</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">Libro</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">kwargs</span><span class="p">[</span><span class="s1">'libro_pk'</span><span class="p">]))</span>
<span class="n">url_pago</span><span class="p">,</span> <span class="n">pago_paypal</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_generar_pago_paypal</span><span class="p">(</span><span class="n">libro</span><span class="p">)</span>
<span class="c1"># Se añade el identificador del pago a la sesion para que PaypalExecuteView</span>
<span class="c1"># pueda identificar al ususuario posteriorment</span>
<span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="s1">'payment_id'</span><span class="p">]</span> <span class="o">=</span> <span class="n">pago_paypal</span><span class="o">.</span><span class="n">id</span>
<span class="c1"># Por ultimo salvar la informacion del pago para poder determinar que</span>
<span class="c1"># libro le corresponde, al terminar la transaccion.</span>
<span class="n">PagoPaypal</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">crear_pago</span><span class="p">(</span><span class="n">pago_paypal</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">libro</span><span class="p">)</span>
<span class="k">return</span> <span class="n">url_pago</span>
</pre></div>
<p>Esta vista se encarga de crear un pago para el libro con el identificador proporcionado,
y redirigir al usuario a la dirección proporcionada por PayPal para completar la transacción.</p>
<p>Para implementar esta vista he usado una <strong>Class Based View</strong> que hereda de
<a class="reference external" href="http://ccbv.co.uk/projects/Django/1.8/django.views.generic.base/RedirectView/">RedirectView</a>,
y he redefinido el método <strong>get_redirect_url</strong>, para que obtenga el modelo del libro seleccionado,
genere un pago de Paypal con los datos de ese libro, y por último devuelva la dirección a la
que el usuario debe ser redirigido para finalizar el pago.</p>
<p>En la página de PayPal aparecerá el título del libro, su número de artículo, y el precio.</p>
<img alt="Imagen de la pagina de paypal para el pago de un libro" src="https://secnot.com/images/django-shop-paypal-rest-1/paypal-screenshot.jpg" />
<p>Una vez el usuario termina el pago es redirigido a la url de retorno, en la que se
debe "ejecutar" el pago para finalizar la transacción, esta dirección de retorno
se especificó en el método <strong>_generar_peticion_pago_paypal()</strong>, de la clase
<strong>PaypalView</strong>.</p>
<div class="highlight"><pre><span></span><span class="s2">"return_url"</span><span class="p">:</span> <span class="n">settings</span><span class="o">.</span><span class="n">SITE_URL</span><span class="o">+</span><span class="n">reverse</span><span class="p">(</span><span class="s1">'aceptar-pago-paypal'</span><span class="p">),</span>
</pre></div>
<p>En esta url se encuentra la vista <strong>PaypalExecuteView</strong> que acepta el pago, guarda la
información del cliente, marca el registro como pagado, y envía el email que contiene
el libro a la dirección de email del usuario proporcionada por PayPal.</p>
<div class="highlight"><pre><span></span><span class="c1"># pagos.views.py</span>
<span class="k">class</span> <span class="nc">PaypalExecuteView</span><span class="p">(</span><span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'pagos/paypal_exito.html'</span>
<span class="k">def</span> <span class="nf">_enviar_ebook_email</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">registro_pago</span><span class="p">):</span>
<span class="sd">"""Enviar Email con el libro al cliente """</span>
<span class="c1">#TODO: adjuntar los archivos del modelo libro</span>
<span class="n">libro</span> <span class="o">=</span> <span class="n">registro_pago</span><span class="o">.</span><span class="n">libro</span>
<span class="n">mensaje</span> <span class="o">=</span> <span class="s2">"Gracias por su compra de </span><span class="si">%s</span><span class="s2">"</span> <span class="o">%</span> <span class="n">libro</span><span class="o">.</span><span class="n">titulo</span>
<span class="n">send_mail</span><span class="p">(</span><span class="s1">'TiendaEbook'</span><span class="p">,</span> <span class="n">mensaje</span><span class="p">,</span>
<span class="n">from_email</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">DEFAULT_FROM_EMAIL</span><span class="p">,</span>
<span class="n">recipient_list</span><span class="o">=</span><span class="p">[</span><span class="n">registro_pago</span><span class="o">.</span><span class="n">payer_email</span><span class="p">,])</span>
<span class="k">def</span> <span class="nf">_aceptar_pago_paypal</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">payment_id</span><span class="p">,</span> <span class="n">payer_id</span><span class="p">):</span>
<span class="sd">"""Aceptar el pago del cliente, actualiza el registro con los datos</span>
<span class="sd"> del cliente proporcionados por paypal"""</span>
<span class="n">registro_pago</span> <span class="o">=</span> <span class="n">get_object_or_404</span><span class="p">(</span><span class="n">PagoPaypal</span><span class="p">,</span> <span class="n">payment_id</span><span class="o">=</span><span class="n">payment_id</span><span class="p">)</span>
<span class="n">pago_paypal</span> <span class="o">=</span> <span class="n">paypalrestsdk</span><span class="o">.</span><span class="n">Payment</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">payment_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">pago_paypal</span><span class="o">.</span><span class="n">execute</span><span class="p">({</span><span class="s1">'payer_id'</span><span class="p">:</span> <span class="n">payer_id</span><span class="p">}):</span>
<span class="n">registro_pago</span><span class="o">.</span><span class="n">pagado</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">registro_pago</span><span class="o">.</span><span class="n">payer_id</span> <span class="o">=</span> <span class="n">payer_id</span>
<span class="n">registro_pago</span><span class="o">.</span><span class="n">payer_email</span> <span class="o">=</span> <span class="n">pago_paypal</span><span class="o">.</span><span class="n">payer</span><span class="p">[</span><span class="s1">'payer_info'</span><span class="p">][</span><span class="s1">'email'</span><span class="p">]</span>
<span class="n">registro_pago</span><span class="o">.</span><span class="n">save</span><span class="p">()</span><span class="k">else</span><span class="p">:</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">HttpResponseBadRequest</span>
<span class="k">return</span> <span class="n">registro_pago</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="sd">"""Extraer identificacion de paypal del cliente, la id del pago,</span>
<span class="sd"> aceptar el pago, y enviar el email."""</span>
<span class="n">context</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">payer_id</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">GET</span><span class="p">[</span><span class="s1">'PayerID'</span><span class="p">]</span>
<span class="n">payment_id</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="p">[</span><span class="s1">'payment_id'</span><span class="p">]</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">HttpResponseBadRequest</span>
<span class="n">registro_pago</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_aceptar_pago_paypal</span><span class="p">(</span><span class="n">payment_id</span><span class="p">,</span> <span class="n">payer_id</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_enviar_ebook_email</span><span class="p">(</span><span class="n">registro_pago</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_to_response</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
</pre></div>
<p>Y eso es todo, el código está disponible en <a class="reference external" href="https://github.com/secnot/tutorial-tienda-django-paypal-1">Github</a>.</p>
<p>En la próxima entrega de este tutorial mejoraré algunas de las deficiencias de la aplicación,
añadiendo un carro de la compra para simplificar la compra de varios libros, y un sistema de
descarga para no tener que enviar el libro por correo.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="https://github.com/paypal/PayPal-Python-SDK">Paypal Python SDK</a></li>
<li><a class="reference external" href="https://developer.paypal.com/docs/api/">REST API Reference</a></li>
<li><a class="reference external" href="http://bootstrapzero.com/bootstrap-template/e-commerce">Template de bootstrap usado</a></li>
</ul>
</div>
Como comprimir directorios en Linux2015-04-13T12:04:00+02:002015-04-13T12:04:00+02:00SecNottag:secnot.com,2015-04-13:/comprimir-directorio-linux.html<p class="first last">Como usar tar, gzip, y zip para comprimir archivos y directorios en linux</p>
<p>Comprimir directorios en Linux es imprescindible para muchas tareas de backup y
administración, en éste artículo se muestran dos métodos usando los formatos
<strong>tar.gz</strong> y <strong>zip</strong>.</p>
<div class="section" id="tar-gzip">
<h2>Tar Gzip</h2>
<p>El formato mas usado en sistemas Unix/Linux es <strong>tar.gz</strong> que es un proceso de dos
pasos primero se usa el programa <strong>tar</strong> para unir todos los archivos a comprimir
en uno solo, sobre el que luego se usa el compresor <strong>gzip</strong>. Esta secuencia es
tan común que el comando <strong>tar</strong> incluye una opción para comprimir directamente el
archivo al finalizar.</p>
<p>Puede comprimir un directorio usando:</p>
<div class="highlight"><pre><span></span>$ tar -czvf nombre-directorio.tar.gz nombre-directorio
</pre></div>
<p>Donde</p>
<ul class="simple">
<li><strong>-z</strong> Comprime el archivo usando gzip</li>
<li><strong>-c</strong> Crea un archivo</li>
<li><strong>-v</strong> Verbose, escribe en pantalla información sobre el proceso de compresión</li>
<li><strong>-f</strong> Nombre del archivo</li>
</ul>
<p>Imagina que quieres comprimir tu directorio <em>home</em></p>
<div class="highlight"><pre><span></span>$ tar -czvf backup-directorio-usuario.tar.gz /home/usuario
</pre></div>
<p>También puedes comprimir todos los archivos dentro de directorio actual
incluidos subdirectorios usando:</p>
<div class="highlight"><pre><span></span>$ tar -czvf nombre-backup.tar.gz *
</pre></div>
<p>Para restaurar un archivo comprimido:</p>
<div class="highlight"><pre><span></span>$ tar -xzvf backup-directorio-usuario.tar.gz
</pre></div>
<p>Donde</p>
<ul class="simple">
<li><strong>-x</strong> Indica que debe extraer los archivos</li>
</ul>
</div>
<div class="section" id="zip">
<h2>Zip</h2>
<p>Algunas veces es necesario que el archivo sea descomprimido en otros sistemas
operativos, en ese caso es útil usar el formato <strong>zip</strong> que tiene mayor
compatibilidad.</p>
<p>Comprimir varios archivos en un solo <strong>zip</strong>:</p>
<div class="highlight"><pre><span></span>$ zip archivos-comprimidos.zip archivo1 archivo2 archivo3
</pre></div>
<p>Comprimir todos los archivos del directorio sin incluir subdirectorios:</p>
<div class="highlight"><pre><span></span>$ zip archivos.zip *
</pre></div>
<p>Comprimir un directorio completo incluyendo subdirectorios:</p>
<div class="highlight"><pre><span></span>$ zip -r directorio-comprimido.zip /home/usuario
</pre></div>
<p>Puedes descomprimir archivos <strong>zip</strong> usando:</p>
<div class="highlight"><pre><span></span>$ unzip archivo.zip
</pre></div>
</div>
Eliminar el subrayado de los enlaces en html2015-04-05T13:04:00+02:002015-04-05T13:04:00+02:00SecNottag:secnot.com,2015-04-05:/css-subrayado-enlaces.html<p class="first last">Como eliminar el subrayado por defecto de los enlaces html.</p>
<p>Un problema que me encuentro una vez tras otra cuando estoy editando CSS, es la
necesidad de eliminar el subrayado por defecto de los enlaces HTML.</p>
<p>La solución es muy sencilla pero siempre se me olvida y tengo que buscar las propiedades
CSS. Esta vez va a ser la última, aqui dejo el código para futuras consultas:</p>
<div class="highlight"><pre><span></span><span class="nt">a</span> <span class="p">{</span>
<span class="k">border-bottom</span><span class="p">:</span> <span class="kc">none</span> <span class="cp">!important</span><span class="p">;</span>
<span class="k">text-decoration</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">a</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
<span class="k">border-bottom</span><span class="p">:</span> <span class="kc">none</span> <span class="cp">!important</span><span class="p">;</span>
<span class="k">text-decoration</span><span class="p">:</span> <span class="kc">none</span><span class="p">,</span>
<span class="p">}</span>
</pre></div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://www.w3schools.com/css/css_link.asp">w3schools Styling Links</a></li>
</ul>
</div>
Configurar Swap en Linux2015-03-23T20:04:00+01:002015-03-23T20:04:00+01:00SecNottag:secnot.com,2015-03-23:/configurar-swap.html<p class="first last">Como y porque configurar espacio swap en Linux.</p>
<p>Con el auge de los servidores baratos, ya sea VPS o dedicados, cada vez es
mas común que cualquier pequeña página tenga que administrar su propio
servidor, un aspecto que es fácilmente ignorado o incluso olvidado es el
espacio swap, y puede tener una grandes repercusiones en el rendimiento.</p>
<div class="section" id="que-es-el-espacio-swap">
<h2>¿Que es el espacio swap?</h2>
<p>Swap es usado cuando Linux llena toda la memoria RAM física disponible. Si el
sistema tiene swap habilitado, datos almacenados en RAM que no están siendo
usados son movidos a swap temporalmente para liberar espacio. Con esto se
consigue que en momentos puntuales en los que la carga del sistema es alta,
exista espacio extra hasta que pase la congestión.</p>
<p>El espacio swap tiene sus limitaciones, al estar normalmente localizado en
algún medio de almacenamiento masivo, ya sea un disco duro o SSD, tienen
un tiempo de acceso y velocidad de transferencia ordenes de magnitud peores
que la memoria RAM. Por eso un sistema que usa swap de manera continuada
tendrá un mal rendimiento, e indica que no tiene suficiente memoria RAM y
es momento de una ampliación.</p>
</div>
<div class="section" id="id1">
<h2>Configurar swap en Linux</h2>
<p>El primer paso es comprobar si swap ya está activado en el sistema.</p>
<div class="highlight"><pre><span></span>$ free -m
total used free shared buffers cached
Mem: <span class="m">7709</span> <span class="m">5009</span> <span class="m">2699</span> <span class="m">312</span> <span class="m">55</span> <span class="m">1198</span>
-/+ buffers/cache: <span class="m">756</span> <span class="m">3953</span>
Swap: <span class="m">0</span> <span class="m">0</span> <span class="m">0</span>
</pre></div>
<p>Con <em>free</em> podemos ver la memoria disponible en el sistema, en caso de que swap
no esté activado deberá aparecer ceros en la linea de swap.</p>
<p>Si no esta activado el siguiente paso es crear el archivo que se usara como swap</p>
<div class="highlight"><pre><span></span>$ sudo dd <span class="k">if</span><span class="o">=</span>/dev/zero <span class="nv">of</span><span class="o">=</span>/swapfile <span class="nv">bs</span><span class="o">=</span>8G <span class="nv">count</span><span class="o">=</span><span class="m">4</span>
$ sudo chmod <span class="m">600</span> /swapfile
</pre></div>
<p>Esto crea un archivo de 8GB (recomendado que su tamaño sea el doble que la RAM),
y cambia los permisos para que no pueda ser leído mas que por el root. Tras esto
hay que crear el espacio swap en el archivo.</p>
<div class="highlight"><pre><span></span>$ sudo mkswap /swapfile
Configurando espacio de intercambio versión <span class="m">1</span>, <span class="nv">tamaño</span> <span class="o">=</span> <span class="m">4194300</span> kiB
sin etiqueta, <span class="nv">UUID</span><span class="o">=</span>7f2020e5-0a09-4f1b-b0af-3053e94f17e2
</pre></div>
<p>Para activar swap</p>
<div class="highlight"><pre><span></span>$ sudo swapon /swapfile
</pre></div>
<p>Antes de continuar es recomendable usar <em>free</em> para comprobar que todo ha funcionado correctamente.</p>
<div class="highlight"><pre><span></span>$ free -m
total used free shared buffers cached
Mem: <span class="m">7709</span> <span class="m">6626</span> <span class="m">1082</span> <span class="m">463</span> <span class="m">189</span> <span class="m">2464</span>
-/+ buffers/cache: <span class="m">350</span> <span class="m">3736</span>
Swap: <span class="m">7980</span> <span class="m">1029</span> <span class="m">6951</span>
</pre></div>
<p>Por último es necesario configurar <strong>fstab</strong> para que swap se active al
arrancar el sistema. Con tu editor favorito añade lo siguiente a fstab.</p>
<div class="highlight"><pre><span></span>/swapfile none swap sw <span class="m">0</span> <span class="m">0</span>
</pre></div>
<p>Y eso es todo, un tutorial MUY básico de como activar swap.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://www.linux.com/news/software/applications/8208-all-about-linux-swap-space">All About Swap Space</a></li>
<li><a class="reference external" href="https://www.digitalocean.com/community/tutorials/how-to-add-swap-on-ubuntu-14-04">How to add swap to Ubuntu 14.04</a></li>
</ul>
</div>
Comandos Útiles2015-03-23T16:04:00+01:002015-04-15T15:45:00+02:00SecNottag:secnot.com,2015-03-23:/commandos-utiles.html<p class="first last">Resumen de comandos útiles para la administración de linux.</p>
<p><strong>Aviso:</strong> <em>Este es un listado de comandos que constantemente olvido y
busco, asi que los he reunido en una lista para tener acceso rápido.</em></p>
<div class="section" id="compresion-de-directorios">
<h2>Compresión de directorios</h2>
<p>Comprimir:</p>
<div class="highlight"><pre><span></span>$ tar -zcvf backup-2013-05-03.tar.gz /home/backup/directory
</pre></div>
<p>Descomprimir:</p>
<div class="highlight"><pre><span></span>$ tar -zxvf backup-2013-05-03.tar.gz
</pre></div>
</div>
<div class="section" id="vim">
<h2>Vim</h2>
<p>Encontrar cada ocurrencia de la cadena 'foo' en el texto, y substituirla por la
cadena 'bar'.</p>
<div class="highlight"><pre><span></span>:%s/foo/bar/g
</pre></div>
<p>Encontrar cada ocurrencia de la cadena 'foo' en la línea actual, y substituirla
por 'bar'.</p>
<div class="highlight"><pre><span></span>:s/foo/bar/g
</pre></div>
<p>Substituir cada ocurrencia de la cadena 'foo' por 'bar', pero pidiendo confirmación
primero.</p>
<div class="highlight"><pre><span></span>:%s/foo/bar/gc
</pre></div>
</div>
<div class="section" id="grep">
<h2>Grep</h2>
<p>Buscar archivos que contengan determinado texto.</p>
<div class="highlight"><pre><span></span>$ grep <span class="s1">'nombre'</span> *.txt
$ grep <span class="s1">'#include<example.h>'</span> *.c
</pre></div>
</div>
<div class="section" id="debug">
<h2>Debug</h2>
<p>Resumen de llamadas al systema realizadas por un comand</p>
<div class="highlight"><pre><span></span>$ strace -c ls >/dev/null
</pre></div>
<p>Trafico de red excepto ssh</p>
<div class="highlight"><pre><span></span>$ tcpdump not port <span class="m">22</span>
</pre></div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://vim.rtorr.com/">Vim Cheat Sheet</a></li>
<li><a class="reference external" href="http://www.pixelbeat.org/cmdline.html">Linux Commands Cheat Sheet</a></li>
</ul>
</div>
Formulario Login usando Class Based Views2015-01-01T15:49:00+01:002015-01-01T15:49:00+01:00SecNottag:secnot.com,2015-01-01:/django-custom-login-cbv.html<p class="first last">Logins en Django usando Class Based Views</p>
<p>Recientemente he tenido que crear un panel de control para una aplicación
django, su función era mostrar estadísticas de los pedidos. Como en
casi todos los casos el primer problema fue crear un sistema de login, esta
es una situación muy común que se presenta en la mayoría de aplicaciones,
la diferencia es que en este caso me decidí por usar <strong>Class Based Views</strong>,
y me he quedado sorprendido con la simplicidad y limpieza del código:</p>
<pre class="code python literal-block">
<span class="c1"># views.py</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">FormView</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">,</span> <span class="n">RedirectView</span>
<span class="c1"># Authentication imports</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">login</span><span class="p">,</span> <span class="n">logout</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.forms</span> <span class="kn">import</span> <span class="n">AuthenticationForm</span>
<span class="kn">from</span> <span class="nn">django.core.urlresolvers</span> <span class="kn">import</span> <span class="n">reverse_lazy</span>
<span class="kn">from</span> <span class="nn">cesta.models</span> <span class="kn">import</span> <span class="n">Pedido</span>
<span class="k">class</span> <span class="nc">LoginView</span><span class="p">(</span><span class="n">FormView</span><span class="p">):</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">AuthenticationForm</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s2">"control_panel/login.html"</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s2">"panel-dashboard"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">():</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_success_url</span><span class="p">())</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">login</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="o">.</span><span class="n">get_user</span><span class="p">())</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">LogoutView</span><span class="p">(</span><span class="n">RedirectView</span><span class="p">):</span>
<span class="n">pattern_name</span> <span class="o">=</span> <span class="s1">'panel-login'</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">logout</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LogoutView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">LoginRequiredMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">():</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">'panel-login'</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ControlPanelView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'control_panel/panel.html'</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">ControlPanelView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">context</span><span class="p">[</span><span class="s1">'pedidos_pendientes'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Pedido</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">pendientes</span><span class="p">()</span>
<span class="c1"># ....</span>
<span class="c1"># Recopilar resto de la informacion</span>
<span class="c1"># ....</span>
<span class="k">return</span> <span class="n">context</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/django-custom-login-cbv/views.py">Descargar views.py</a></p>
<pre class="code python literal-block">
<span class="c1"># urls.py</span>
<span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">patterns</span><span class="p">,</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">.views</span> <span class="kn">import</span> <span class="n">ControlPanelView</span><span class="p">,</span> <span class="n">LoginView</span><span class="p">,</span> <span class="n">LogoutView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="n">patterns</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">ControlPanelView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-dashboard"</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^login/$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">LoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-login"</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^logout/$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">LogoutView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-logout"</span><span class="p">),</span>
<span class="p">)</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/django-custom-login-cbv/urls.py">Descargar urls.py</a></p>
<p>Por último un ejemplo para el template <strong>login.html</strong>:</p>
<div class="highlight"><pre><span></span>{% extends 'base.html' %}
{% block content %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"login-panel"</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">"post"</span> <span class="na">role</span><span class="o">=</span><span class="s">"form"</span><span class="p">></span>{% csrf_token %}
{{ form.username }}
{{ form.password }}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">value</span><span class="o">=</span><span class="s">"Login"</span><span class="p">/></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endblock %}
</pre></div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://ccbv.co.uk">Classy Class-Based Views</a></li>
<li><a class="reference external" href="https://docs.djangoproject.com/en/1.7/topics/auth/default/">Using the Django authentication system</a></li>
<li><a class="reference external" href="https://github.com/stefanfoulis/django-class-based-auth-views/">Stefanfoulis Class Based Auth Views</a></li>
</ul>
</div>
Registro usuarios en Django2015-01-01T15:49:00+01:002015-01-01T15:49:00+01:00SecNottag:secnot.com,2015-01-01:/django-registro-usuarios.html<p class="first last">Logins en Django usando Class Based Views</p>
<p>Recientemente he tenido que crear un panel de control para una aplicación
django, su función era mostrar estadísticas de los pedidos. Como en
casi todos los casos el primer problema fue crear un sistema de login, esta
es una situación muy común que se presenta en la mayoría de aplicaciones,
la diferencia es que en este caso me decidí por usar <strong>Class Based Views</strong>,
y me he quedado sorprendido con la simplicidad y limpieza del código:</p>
<pre class="code python literal-block">
<span class="c1"># views.py</span>
<span class="kn">from</span> <span class="nn">django.views.generic</span> <span class="kn">import</span> <span class="n">FormView</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">,</span> <span class="n">RedirectView</span>
<span class="c1"># Authentication imports</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth</span> <span class="kn">import</span> <span class="n">login</span><span class="p">,</span> <span class="n">logout</span>
<span class="kn">from</span> <span class="nn">django.contrib.auth.forms</span> <span class="kn">import</span> <span class="n">AuthenticationForm</span>
<span class="kn">from</span> <span class="nn">django.core.urlresolvers</span> <span class="kn">import</span> <span class="n">reverse_lazy</span>
<span class="kn">from</span> <span class="nn">cesta.models</span> <span class="kn">import</span> <span class="n">Pedido</span>
<span class="k">class</span> <span class="nc">LoginView</span><span class="p">(</span><span class="n">FormView</span><span class="p">):</span>
<span class="n">form_class</span> <span class="o">=</span> <span class="n">AuthenticationForm</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s2">"control_panel/login.html"</span>
<span class="n">success_url</span> <span class="o">=</span> <span class="n">reverse_lazy</span><span class="p">(</span><span class="s2">"panel-dashboard"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">():</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">get_success_url</span><span class="p">())</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
<span class="n">login</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="p">,</span> <span class="n">form</span><span class="o">.</span><span class="n">get_user</span><span class="p">())</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">LogoutView</span><span class="p">(</span><span class="n">RedirectView</span><span class="p">):</span>
<span class="n">pattern_name</span> <span class="o">=</span> <span class="s1">'panel-login'</span>
<span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">logout</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LogoutView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">LoginRequiredMixin</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">dispatch</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">request</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">is_authenticated</span><span class="p">():</span>
<span class="k">return</span> <span class="n">HttpResponseRedirect</span><span class="p">(</span><span class="n">reverse</span><span class="p">(</span><span class="s1">'panel-login'</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="nb">super</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">dispatch</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ControlPanelView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">TemplateView</span><span class="p">):</span>
<span class="n">template_name</span> <span class="o">=</span> <span class="s1">'control_panel/panel.html'</span>
<span class="k">def</span> <span class="nf">get_context_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">context</span> <span class="o">=</span> <span class="nb">super</span><span class="p">(</span><span class="n">ControlPanelView</span><span class="p">,</span> <span class="bp">self</span><span class="p">)</span><span class="o">.</span><span class="n">get_context_data</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
<span class="n">context</span><span class="p">[</span><span class="s1">'pedidos_pendientes'</span><span class="p">]</span> <span class="o">=</span> <span class="n">Pedido</span><span class="o">.</span><span class="n">objects</span><span class="o">.</span><span class="n">pendientes</span><span class="p">()</span>
<span class="c1"># ....</span>
<span class="c1"># Recopilar resto de la informacion</span>
<span class="c1"># ....</span>
<span class="k">return</span> <span class="n">context</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/django-custom-login-cbv/views.py">Descargar views.py</a></p>
<pre class="code python literal-block">
<span class="c1"># urls.py</span>
<span class="kn">from</span> <span class="nn">django.conf.urls</span> <span class="kn">import</span> <span class="n">patterns</span><span class="p">,</span> <span class="n">url</span>
<span class="kn">from</span> <span class="nn">.views</span> <span class="kn">import</span> <span class="n">ControlPanelView</span><span class="p">,</span> <span class="n">LoginView</span><span class="p">,</span> <span class="n">LogoutView</span>
<span class="n">urlpatterns</span> <span class="o">=</span> <span class="n">patterns</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">ControlPanelView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-dashboard"</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^login/$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">LoginView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-login"</span><span class="p">),</span>
<span class="n">url</span><span class="p">(</span>
<span class="n">regex</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">'^logout/$'</span><span class="p">,</span>
<span class="n">view</span> <span class="o">=</span> <span class="n">LogoutView</span><span class="o">.</span><span class="n">as_view</span><span class="p">(),</span>
<span class="n">name</span> <span class="o">=</span> <span class="s2">"panel-logout"</span><span class="p">),</span>
<span class="p">)</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/django-custom-login-cbv/urls.py">Descargar urls.py</a></p>
<p>Por último un ejemplo para el template <strong>login.html</strong>:</p>
<div class="highlight"><pre><span></span>{% extends 'base.html' %}
{% block content %}
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"login-panel"</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="o">=</span><span class="s">"post"</span> <span class="na">role</span><span class="o">=</span><span class="s">"form"</span><span class="p">></span>{% csrf_token %}
{{ form.username }}
{{ form.password }}
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">value</span><span class="o">=</span><span class="s">"Login"</span><span class="p">/></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
{% endblock %}
</pre></div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://ccbv.co.uk">Classy Class-Based Views</a></li>
<li><a class="reference external" href="https://docs.djangoproject.com/en/1.7/topics/auth/default/">Using the Django authentication system</a></li>
<li><a class="reference external" href="https://github.com/stefanfoulis/django-class-based-auth-views/">Stefanfoulis Class Based Auth Views</a></li>
</ul>
</div>
Desplegar django con docker, nginx y gunicorn2014-09-18T22:33:00+02:002014-09-18T22:33:00+02:00SecNottag:secnot.com,2014-09-18:/docker-nginx-gunicorn-django.html<p class="first last">Una breve guía sobre como desplegar django usando docker.</p>
<p>Hace unos meses publiqué un pequeño tutorial sobre el
<a class="reference external" href="https://secnot.com/django-nginx-gunicorn.html">despliegue de aplicaciones Django</a>, en
el que mencioné <a class="reference external" href="http://www.docker.io/">Docker</a>, pero no expliqué ni su
funcionamiento ni sus ventajas, principalmente porque pensaba escribir un
tutorial que por falta de tiempo se quedó en el tintero. Desde entonces han
aparecido algunos buenos manuales, así que en su lugar he escrito este tutorial
sobre como desplegar django usando Docker.</p>
<p>El tutorial asume que el lector posee conocimientos sobre la configuración
de nginx y gunicorn, así como del funcionamiento de docker.
Si no es así recomiendo leer mi tutorial
<a class="reference external" href="https://secnot.com/django-nginx-gunicorn.html">despliegue de aplicaciones Django</a>,
y algún tutorial de docker, antes de continuar.</p>
<div class="section" id="introduccion">
<h2>Introducción</h2>
<p>En esencia el proceso de crear una imagen Docker es el mismo que hay que
seguir para configurar cualquier servidor manualmente, cada uno de los pasos
en el proceso se plasma en un archivo <strong>Dockerfile</strong> que es usado por Docker
para construir la imagen.
Por ejemplo para un contenedor de una aplicación django, será necesario:</p>
<ul class="simple">
<li>Copiar la aplicación django al contenedor, e instalar sus dependencias.</li>
<li>Instalar y configurar gunicorn.</li>
<li>Instalar y configurar nginx.</li>
<li>Instalar supervisor y configurarlo para que arranque nginx y gunicorn.</li>
</ul>
<p>Para conseguir todo esto, además del archivo <strong>Dockerfile</strong>, necesitamos los
archivos de configuración de nginx, gunicorn, supervisor, y la aplicación
django que se copiarán a la imagen. Todos estos archivos del proyecto deben
estar en el mismo directorio que Dockerfile, por ejemplo la estructura usada
en este tutorial es:</p>
<div class="highlight"><pre><span></span>Docker/
<span class="p">|</span>--Dockerfile
<span class="p">|</span>--gunicorn-config.py
<span class="p">|</span>--nginx-default
<span class="p">|</span>--supervisor.conf
<span class="p">|</span>--django_app/
<span class="p">|</span> <span class="p">|</span>--manage.py
<span class="p">|</span> <span class="p">|</span>--requirements.txt
<span class="p">|</span> <span class="p">|</span>--static/
<span class="p">|</span> <span class="p">|</span>--app1/
<span class="p">|</span> <span class="p">|</span> <span class="p">|</span>--models.py
<span class="p">|</span> <span class="p">|</span> <span class="p">|</span>--....
<span class="p">|</span> <span class="p">|</span>--app2/
<span class="p">|</span> <span class="p">|</span> <span class="p">|</span>--....
</pre></div>
<p><a class="reference external" href="https://secnot.com/scripts/docker-nginx-gunicorn-django/Docker.zip">Descargar</a></p>
<p>El Dockerfile en este tutorial asume que la aplicación django esta en el
subdirectorio <strong>django_app</strong>, que a su vez contiene el archivo
<strong>requirements.txt</strong> con las dependencias del mismo, y el directorio <strong>static</strong>
con todos los archivos estáticos.</p>
</div>
<div class="section" id="dockerfile">
<h2>Dockerfile</h2>
<p>Toda la magia en la creación de una imagen está en el archivo <strong>Dockerfile</strong>,
su sintaxis es muy clara:</p>
<pre class="code bash literal-block">
FROM ubuntu:14.04
MAINTAINER secnot <secnot@secnot.com>
<span class="c1"># Actualizacion de los 'sources' a la ultima version
</span>RUN apt-get update
<span class="c1"># Instalar los paquetes del sistema necesarios para python
</span>RUN apt-get install -qy python <span class="se">\
</span> python-dev <span class="se">\
</span> python-pip <span class="se">\
</span> python-setuptools <span class="se">\
</span> build-essential
<span class="c1"># Instalar algunas utilidades extras (opcional)
</span>RUN apt-get install -qy vim <span class="se">\
</span> wget <span class="se">\
</span> net-tools <span class="se">\
</span> git
<span class="c1"># Instalamos resto aplicaciones
</span>RUN apt-get install -qy nginx <span class="se">\
</span> supervisor
<span class="c1">###############################
#
# Nginx
#
###############################
</span>
<span class="c1"># Copiar la configuracion de nginx de la aplicion
</span>ADD nginx-default /etc/nginx/sites-available/default
<span class="c1"># Desactiva el modo demonio para arrancar el proceso con supervisor
</span>RUN <span class="nb">echo</span> <span class="s2">"\ndaemon off;"</span> >> /etc/nginx/nginx.conf
<span class="c1"># Cambiar los permisos de nginx para poder ejecutar nginx como www-data
</span>RUN chown -R www-data:www-data /var/lib/nginx
<span class="c1"># Permitir el acceso al puerto 80 del contenedor
</span>EXPOSE <span class="m">80</span>
<span class="c1">##################################
#
# Gunicorn y Django
#
################################
</span>
<span class="c1"># Copiar aplicacion del subdirectorio django_app/ al directorio
# /django_app en el contenedor
</span>ADD django_app /django_app
RUN chown -R www-data:www-data /django_app
<span class="c1"># Si la aplicacion tiene dependencias de paquetes del del sistema
# este es un buen sitio para instalarlas, por ejemplo para PIL:
# RUN apt-get install -qy python-dev libjpeg-dev zlib1g-dev
</span>
<span class="c1"># Usamos requirements.txt para instalar las dependencias de la
# aplicacion.
</span>RUN pip install -r /django_app/requirements.txt
<span class="c1"># Tambien se pueden instalar individualmente, por ejemplo:
# RUN pip install Django
# RUN pip install bleach
# ...
</span>
<span class="c1"># Una buena medida de seguridad es alamacenar claves usuarios y
# otras credenciales de seguridad en variables de entorno.
# Se importan desde settings.py con:
# PAYPAL_CLIENT_ID = os.environ['PAYPAL_CLIENT_ID']
# PAYPAL_CLIENT_SECRET = os.environ['PAYPAL_CLIENT_SECRET']
</span>ENV PAYPAL_CLIENT_ID sdfasFASDRwefasFqasdfAsdfAsdFAsdfsDFaSDfWERtSDFg
ENV PAYPAL_CLIENT_SECRET ASAsdfarasDFaRasdFaSsdfghJdfGHDGsdTRSDfGErtAFSD
<span class="c1"># Como precaucion se instala gunicorn, aunque deberia estar en
# requirements.txt
</span>RUN pip install gunicorn
<span class="c1"># Por ultimo se copia la configuracion de gunicorn.
</span>ADD gunicorn-config.py /etc/gunicorn/config.py
<span class="c1">#############################
#
# Supervisor
#
############################
</span>
<span class="c1"># Copiar la configuracion
</span>ADD supervisor.conf /etc/supervisor/conf.d/django_app.conf
<span class="c1"># Instalamos supervisor-stdout que permite que los logs, sean impresos
# en stdout. (ver supervisor.conf)
</span>RUN pip install supervisor-stdout
<span class="c1"># Establecer el directorio de trabajo
</span>WORKDIR /django_app
<span class="c1"># Comando por defecto que se ejecutara al arranque del contenedor,
# supervisor se encarga de gestionar nginx y gunicorn.
</span>CMD <span class="o">[</span><span class="s2">"/usr/bin/supervisord"</span>, <span class="s2">"-n"</span>, <span class="s2">"-c"</span>, <span class="s2">"/etc/supervisor/supervisord.conf"</span><span class="o">]</span>
</pre>
</div>
<div class="section" id="supervisor-y-logging">
<h2>Supervisor y Logging</h2>
<p>El archivo <strong>supervisor.conf</strong>, contiene la configuración necesaria para
que supervisord arranque nginx y gunicorn, y los reinicie en caso de que mueran:</p>
<div class="highlight"><pre><span></span><span class="o">[</span>program:gunicorn<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
<span class="nv">directory</span> <span class="o">=</span> /django_app
<span class="nv">user</span> <span class="o">=</span> www-data
<span class="nv">autostart</span> <span class="o">=</span> True
<span class="nv">autorestart</span> <span class="o">=</span> True
<span class="o">[</span>program:nginx<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> /usr/sbin/nginx
<span class="nv">autostart</span> <span class="o">=</span> True
<span class="nv">autorestart</span> <span class="o">=</span> True
</pre></div>
<p>Pero esa no es la única función de supervisor, sino que también se encarga de
guardar logs de las salidas <strong>stdout</strong> y <strong>stderr</strong> de cada programa que
gestiona. Con django se puede aprovechar esta característica para
que supervisor se encargue de gestionar los logs, solo es necesario
imprimirlos en <strong>stdout</strong> en lugar de guardarlos en un archivo, editando
<strong>settings.py</strong> de tu aplicación:</p>
<div class="highlight"><pre><span></span><span class="c1"># settings.py</span>
<span class="c1"># .....</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="n">LOGGING</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'version'</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="s1">'dissable_existing_loggers'</span><span class="p">:</span> <span class="kc">False</span><span class="p">,</span>
<span class="s1">'formatters'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'verbose'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'format'</span><span class="p">:</span> <span class="s2">"[</span><span class="si">%(asctime)s</span><span class="s2">] </span><span class="si">%(levelname)s</span><span class="s2"> [</span><span class="si">%(name)s</span><span class="s2">:</span><span class="si">%(lineno)s</span><span class="s2">] </span><span class="si">%(message)s</span><span class="s2">"</span><span class="p">,</span>
<span class="s1">'datefmt'</span><span class="p">:</span> <span class="s2">"%Y-%m-</span><span class="si">%d</span><span class="s2"> %H:%M:%S"</span><span class="p">,</span>
<span class="p">},</span>
<span class="s1">'simple'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'format'</span><span class="p">:</span> <span class="s2">"</span><span class="si">%(levelname)s</span><span class="s2"> </span><span class="si">%(message)s</span><span class="s2">"</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'console'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'INFO'</span><span class="p">,</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'logging.StreamHandler'</span><span class="p">,</span>
<span class="s1">'stream'</span><span class="p">:</span> <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="p">,</span>
<span class="s1">'formatter'</span><span class="p">:</span> <span class="s1">'simple'</span><span class="p">,</span>
<span class="p">},</span>
<span class="s1">'mail_admins'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'ERROR'</span><span class="p">,</span>
<span class="s1">'class'</span><span class="p">:</span> <span class="s1">'django.utils.log.AdminEmailHandler'</span><span class="p">,</span>
<span class="s1">'include_html'</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">},</span>
<span class="s1">'loggers'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">''</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'handlers'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'mail_admins'</span><span class="p">,</span> <span class="s1">'console'</span><span class="p">],</span>
<span class="s1">'level'</span><span class="p">:</span> <span class="s1">'DEBUG'</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">}</span>
</pre></div>
<p>El manejador <strong>StreamHandler</strong> redirige todos los logs de nivel INFO, o superior
a la salida estándar, cuando supervisor recibe estos logs tiene dos opciones:</p>
<ul>
<li><p class="first"><strong>Almacenar los logs en un archivo</strong>, que a su vez puede o no estar montado
mediante un volumen desde el host, para que los datos sean persistente:</p>
<div class="highlight"><pre><span></span><span class="o">[</span>program:gunicorn<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> /usr/local/bin/gunicorn -c /etc/gunicorn/config.py yourapp.wsgi:application
<span class="nv">directory</span> <span class="o">=</span> /django_app
<span class="nv">user</span> <span class="o">=</span> www-data
<span class="nv">autostart</span> <span class="o">=</span> True
<span class="nv">autorestart</span> <span class="o">=</span> True
<span class="nv">redirect_stderr</span><span class="o">=</span><span class="nb">true</span>
<span class="nv">stdout_logfile</span><span class="o">=</span>/var/log/django.log
</pre></div>
<p>Para usar un volumen, se debe iniciar el contenedor con la opción <strong>-v</strong>:</p>
<div class="highlight"><pre><span></span>$ docker run -v /var/log/django.log:/var/log/django.log -p <span class="m">80</span>:80 yourapp:nginx
</pre></div>
</li>
<li><p class="first"><strong>Redirigir los logs a stdout</strong> para que sean gestionados por el host
que inicio el contenedor. Para esto es necesario usar el manejador de eventos
<a class="reference external" href="https://github.com/coderanger/supervisor-stdout">supervisor-stdout</a>.
Y la configuración de supervisor seria:</p>
<pre class="code bash literal-block">
<span class="o">[</span>supervisord<span class="o">]</span>
<span class="nv">nodaemon</span> <span class="o">=</span> <span class="nb">true</span>
<span class="o">[</span>program:gunicorn<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> /usr/local/bin/gunicorn -c /etc/gunicorn/config.py testsite.wsgi:application
<span class="nv">directory</span> <span class="o">=</span> /django_app
<span class="nv">user</span> <span class="o">=</span> www-data
<span class="nv">autostart</span> <span class="o">=</span> True
<span class="nv">autorestart</span> <span class="o">=</span> True
<span class="nv">stdout_events_enabled</span> <span class="o">=</span> <span class="nb">true</span>
<span class="nv">stderr_events_enabled</span> <span class="o">=</span> <span class="nb">true</span>
<span class="o">[</span>program:nginx<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> /usr/sbin/nginx
<span class="nv">autostart</span> <span class="o">=</span> True
<span class="nv">autorestart</span> <span class="o">=</span> True
<span class="nv">stdout_events_enabled</span> <span class="o">=</span> <span class="nb">true</span>
<span class="nv">stderr_events_enabled</span> <span class="o">=</span> <span class="nb">true</span>
<span class="o">[</span>eventlistener:stdout<span class="o">]</span>
<span class="nb">command</span> <span class="o">=</span> supervisor_stdout
<span class="nv">buffer_size</span> <span class="o">=</span> <span class="m">100</span>
<span class="nv">events</span> <span class="o">=</span> PROCESS_LOG
<span class="nv">result_handler</span> <span class="o">=</span> supervisor_stdout:event_handler
</pre>
<p>Para arrancar el contenedor, hay que añadir la opción <strong>-a stdout</strong> para que
imprima la salida en stdout:</p>
<div class="highlight"><pre><span></span>$ sudo docker run --rm -a stdout -p <span class="m">80</span>:80 yourapp:nginx
</pre></div>
<p>En el host se puede configurar supervisor para que arranque el contenedor, y
al tiempo guardar los logs</p>
</li>
</ul>
</div>
<div class="section" id="seguridad">
<h2>Seguridad</h2>
<p><strong>Los contenedores docker NO son seguros</strong>, a pesar de lo que hayas leído es
posible salir de un contenedor.
Por diseño los contenedores comparten el mismo kernel que el host, y pueden
hacer llamadas de sistema al mismo, de manera que cualquier vulnerabilidad
del interfaz del kernel, es explotable desde un contenedor.
La mejor política es tratar docker como una herramienta para desplegar
aplicaciones de forma sencilla, que además proporciona una capa extra de
seguridad. Dicho esto, es posible mejorar la seguridad con medidas sencillas:</p>
<ul>
<li><p class="first"><strong>NO ejecutes como root</strong> los procesos dentro del contenedor, usa
un usuario sin privilegios:</p>
<div class="highlight"><pre><span></span>$ docker run -u<span class="o">=</span>www-data yourapp:nginx
</pre></div>
<p>con <strong>-u</strong> hay que indicar un usuario o <em>uid</em> existente en el contenedor.</p>
</li>
<li><p class="first"><strong>Limita la memoria</strong> disponible para los procesos del contenedor con la
opción <strong>-m</strong>, por ejemplo 200MB:</p>
<div class="highlight"><pre><span></span>$ docker run -m<span class="o">=</span>200m yourapp:nginx
</pre></div>
</li>
<li><p class="first"><strong>Limita el uso de cpu</strong>, permitiendo la ejecución únicamente en los
nucleos/cpus especificados con <strong>--cpuset</strong>:</p>
<div class="highlight"><pre><span></span>$ docker run --cpuset<span class="o">=</span><span class="m">0</span>,1 yourapp:nginx
</pre></div>
</li>
<li><p class="first"><strong>NO uses volúmenes</strong> para montar el sistema de archivos del host en el
contenedor, es cómodo pero es difícil de configurar correctamente
para que sea seguro. Si no tiene mas remedio que usarlos, móntalos en modo
lectura:</p>
<div class="highlight"><pre><span></span>$ docker run -v /host/static:/container/static:ro
</pre></div>
</li>
<li><p class="first"><strong>Utiliza versiones reciente del kernel</strong>, como he comentado anteriormente
todas las vulnerabilidades del kernel son explotables desde el contenedor,
instala versiones recientes y esta atento a la aparición de nuevos exploits.</p>
</li>
<li><p class="first"><strong>Usa una máquina virtual</strong> en la que ejecutar docker, de esta manera tienes
lo mejor de los dos mundos, la seguridad de una <strong>VM</strong>, con la facilidad
para desplegar aplicaciones de docker.</p>
</li>
</ul>
</div>
<div class="section" id="comandos-utiles">
<h2>Comandos útiles</h2>
<p>Una vez la aplicación esté correctamente configurada, puedes construir el
contenedor ejecutando el siguiente comando desde el directorio <em>Docker</em>:</p>
<div class="highlight"><pre><span></span>$ sudo docker build --rm:True -t yourapp:nginx .
</pre></div>
<p>y deberia aparecer al listar las imágenes disponibles:</p>
<div class="highlight"><pre><span></span>$ sudo docker.io images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
yourapp nginx fb3367f96602 <span class="m">4</span> minutes ago <span class="m">544</span>.4 MB
ubuntu <span class="m">14</span>.04 826633116fdc <span class="m">5</span> days ago <span class="m">194</span>.2 MB
</pre></div>
<p>y por ultimo se ejecuta el contenedor con:</p>
<div class="highlight"><pre><span></span>$ sudo docker run --rm -a stdout -p <span class="m">80</span>:80 yourapp:nginx
</pre></div>
<p>El parámetro <strong>-p 80:80</strong> le indica a docker que publique el puerto 80 del
contenedor en el puerto 80 local, de forma que el contenedor sea accesible
desde el exterior. <strong>-rm</strong> indica que el contenedor debe ser eliminado
una vez finalice.</p>
<p>Por defecto para crear y ejecutar contenedores es necesario, tener privilegios
de root, esto es debido a que el demonio docker se ejecuta como root,
y solo es posible comunicarse con el a través de de un socket unix, propiedad
del usuario root y el grupo docker.</p>
<p>Para que un usuario sea capaz de manejar contenedores, solo es necesario
añadirlo al grupo docker, y reiniciar el demonio:</p>
<div class="highlight"><pre><span></span>$ sudo usermod -a -G docker gowen
$ sudo service docker restart
</pre></div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://serversforhackers.com/articles/2014/03/20/getting-started-with-docker/">Getting Started with docker</a></li>
<li><a class="reference external" href="http://www.docker.io/">Docker</a></li>
<li><a class="reference external" href="http://mmckeen.net/blog/2013/12/14/docker-all-the-things-nginx-and-supervisor/">Docker all the things</a></li>
<li><a class="reference external" href="https://sentry.readthedocs.org/en/latest/">Sentry readthedocs.org</a></li>
<li><a class="reference external" href="https://goldmann.pl/blog/2014/07/18/logging-with-the-wildfly-docker-image/">Logging with docker</a></li>
<li><a class="reference external" href="http://supervisord.org/">Supervisord</a></li>
<li><a class="reference external" href="http://www.slideshare.net/jpetazzo/linux-containers-lxc-docker-and-security">Linux containers lxc docker and security</a></li>
</ul>
</div>
Variables de entorno en virtualenv2014-06-25T20:00:00+02:002014-06-25T20:00:00+02:00SecNottag:secnot.com,2014-06-25:/django-virtualenv-variables-entorno.html<p class="first last">Variables de entorno con virtualenv</p>
<p>Un problema muy común en cualquier aplicación django es la necesidad de
almacenar algún dato confidencial para su funcionamiento, por ejemplo
una clave, nombre de usuario, o identificador de un API, esto se suele hacer en el archivo de configuración <strong>settings.py</strong>, pero es una mala practica de
seguridad.</p>
<p>La solución es definir variables de entorno en el shell, e importarlas desde
<strong>settings.py</strong>, de manera que sea mas difícil que un fallo exponga la
información.</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1">#.bashrc</span>
<span class="nb">export</span> <span class="nv">EMAIL_HOST_USER</span><span class="o">=</span><span class="s2">"tuemail@gmail.com"</span>
<span class="nb">export</span> <span class="nv">EMAIL_HOST_PASSWORD</span><span class="o">=</span><span class="s2">"tuclave"</span>
</pre></div>
<div class="highlight"><pre><span></span><span class="c1">#settings.py</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="n">EMAIL_HOST_USER</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s1">'EMAIL_HOST_USER'</span><span class="p">]</span>
<span class="n">EMAIL_HOST_PASSWORD</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s1">'EMAIL_HOST_PASSWORD'</span><span class="p">]</span>
</pre></div>
<p>La limitación de este sistema cuando se está usando <strong>virtualenv</strong>, es que no
permite tener distintos valores de una variable para cada entorno. Esto se
puede solucionar usando los <em>hooks</em> <strong>.virtualenvs/app_env/bin/postactivate</strong>
para establecer las variables al entrar en el entorno:</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1"># This hook is run after this virtualenv is activated.</span>
<span class="nb">export</span> <span class="nv">EMAIL_HOST_USER</span><span class="o">=</span><span class="s2">"tuemail@gmail.com"</span>
<span class="nb">export</span> <span class="nv">EMAIL_HOST_PASSWORD</span><span class="o">=</span><span class="s2">"tuclave"</span>
</pre></div>
<p>y <strong>.virtualenvs/app_env/bin/predeactivate</strong> para limpiarlas al salir:</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="c1"># This hook is run before this virtualenv is deactivated.</span>
<span class="nb">unset</span> EMAIL_HOST_USER
<span class="nb">unset</span> EMAIL_HOST_PASSWORD
</pre></div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://stackoverflow.com/questions/9554087/setting-an-environment-variable-in-virtualenv">Setting an environment variable in virtualenv</a></li>
<li><a class="reference external" href="https://secnot.com/django-virtualenv.html">Django y virtualenv</a></li>
</ul>
</div>
Cacheando Amazon S3 con Varnish2014-03-22T22:38:00+01:002014-03-22T22:38:00+01:00SecNottag:secnot.com,2014-03-22:/cacheando-amazon-s3-con-varnish.html<p class="first last">Como usar Varnish como frontend de amazon S3 para reducir costes.</p>
<p>Amazon S3 es un gran servicio para alojar los archivos de una página web,
ofrece alta disponibilidad, fiabilidad, y seguridad (configurado correctamente).
Pero en páginas con demasiado tráfico, con una gran cantidad de peticiones,
y/o ancho de banda, como por ejemplo páginas que alojen gran cantidad de
imágenes, puede hacer el coste de S3 prohibitivo.</p>
<p>En estos casos la alternativa es usar un CDN, o gestionar tu propio
sistema de distribución. Un CDN puede resultar algo más barato dependiendo
del tráfico, pero no significativamente. Gestionar tu propio servidor aumenta
la complejidad de la aplicación, los posibles puntos de fallo, es menos
escalable, y más difícil de administrar.</p>
<p>Existe una solución intermedia, que consiste en seguir almacenando los
archivos en S3, y usar <strong>Varnish</strong> como proxy cache. De esta manera cuando
una petición de un archivo llega, <strong>Varnish</strong> lo obtiene de S3, se lo envía
al cliente, y guarda una copia en caché. Sucesivas peticiones de ese archivo
se sirven directamente desde cache.</p>
<p>Con esta configuración no es necesario modificar el <em>backend</em> de almacenamiento
ya que se sigue usando S3, es altamente escalable, facil de administrar, y
permite seguir usando S3 como <em>fallback</em>.</p>
<div class="section" id="configuracion-de-varnish">
<h2>Configuración de Varnish</h2>
<p>Este artículo no es un tutorial de Varnish, si eso es lo que estas buscando,
hay muchos disponibles en la red, pero te recomiendo que empieces por los
enlaces al final del articulo.</p>
<p>Archivo de configuración para Varnish con S3 <strong>default.vcl</strong>:</p>
<pre class="code c literal-block">
<span class="cm">/* Amazon S3 Backend */</span>
<span class="n">backend</span> <span class="n">s3</span> <span class="p">{</span>
<span class="p">.</span><span class="n">host</span> <span class="o">=</span> <span class="s">"nombrebucket.s3.amazonaws.com"</span><span class="p">;</span>
<span class="p">.</span><span class="n">port</span> <span class="o">=</span> <span class="s">"80"</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/* vcl_recv es llamado cada vez que una petición es recibida */</span>
<span class="n">sub</span> <span class="n">vcl_recv</span> <span class="p">{</span>
<span class="cm">/* Solo servimos peticiones GET y HEAD */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">request</span> <span class="o">!=</span> <span class="s">"GET"</span> <span class="o">&&</span> <span class="n">req</span><span class="p">.</span><span class="n">request</span> <span class="o">!=</span> <span class="s">"HEAD"</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">pass</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/* Solo servimos archivos, asi que ni Cookies ni Autorizaciones */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Authorization</span> <span class="o">||</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Cookie</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">pass</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">set</span> <span class="n">req</span><span class="p">.</span><span class="n">grace</span> <span class="o">=</span> <span class="mi">120</span><span class="n">s</span><span class="p">;</span>
<span class="n">set</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">X</span><span class="o">-</span><span class="n">Forwarded</span><span class="o">-</span><span class="n">For</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">ip</span><span class="p">;</span>
<span class="cm">/* Establecer backend para la peticion */</span>
<span class="n">set</span> <span class="n">req</span><span class="p">.</span><span class="n">backend</span> <span class="o">=</span> <span class="n">s3</span><span class="p">;</span>
<span class="n">set</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">host</span> <span class="o">=</span> <span class="s">"nombrebucket.s3.amazonaws.com"</span><span class="p">;</span>
<span class="cm">/* Eliminar de la cabecera todos los campos que pueden afectar la cache */</span>
<span class="n">unset</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Cache</span><span class="o">-</span><span class="n">Control</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Pragma</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Expires</span><span class="p">;</span>
<span class="cm">/* Si el archivo es una imagen desactivamos compresion */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">url</span> <span class="o">~</span> <span class="s">"\.(jpeg|jpg|png|ico|svg|gif)$"</span><span class="p">)</span> <span class="p">{</span>
<span class="n">unset</span> <span class="n">req</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Accept</span><span class="o">-</span><span class="n">Encoding</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span><span class="n">lookup</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">url</span> <span class="o">~</span> <span class="s">"\.(css|js|txt)$"</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">lookup</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/* Demas tipos no soportados */</span>
<span class="k">return</span> <span class="p">(</span><span class="n">pass</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/* Llamado cuando hay un hit en cache */</span>
<span class="n">sub</span> <span class="n">vcl_hit</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">deliver</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/* Llamado cuando hay un miss en cache */</span>
<span class="n">sub</span> <span class="n">vcl_miss</span> <span class="p">{</span>
<span class="k">return</span> <span class="p">(</span><span class="n">fetch</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/* Llamado cuando un documento se ha descargado exitosamente del backend */</span>
<span class="n">sub</span> <span class="n">vcl_fetch</span> <span class="p">{</span>
<span class="cm">/* Se eliminan cookies antes de que el objeto sea introducido en cache */</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Set</span><span class="o">-</span><span class="n">Cookie</span><span class="p">;</span>
<span class="cm">/* Si es una imagen se desactiva compresion (Solo precaucion) */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">url</span> <span class="o">~</span> <span class="s">"\.(jpg|jpeg|png|ico|svg|gif)$"</span><span class="p">)</span> <span class="p">{</span>
<span class="n">set</span> <span class="n">beresp</span><span class="p">.</span><span class="n">do_gzip</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/* Establecer fecha de caducidad para los datos en cache */</span>
<span class="n">set</span> <span class="n">beresp</span><span class="p">.</span><span class="n">ttl</span> <span class="o">=</span> <span class="mi">1</span><span class="n">w</span><span class="p">;</span> <span class="cm">/* Una semana */</span>
<span class="n">set</span> <span class="n">beresp</span><span class="p">.</span><span class="n">grace</span> <span class="o">=</span> <span class="mi">120</span><span class="n">s</span><span class="p">;</span>
<span class="cm">/* Forzar cacheado por el cliente */</span>
<span class="n">set</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Expires</span> <span class="o">=</span> <span class="n">beresp</span><span class="p">.</span><span class="n">ttl</span><span class="p">;</span>
<span class="n">set</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Cache</span><span class="o">-</span><span class="n">Control</span> <span class="o">=</span> <span class="s">"max-age=604800"</span><span class="p">;</span> <span class="cm">/* 7 dias */</span>
<span class="cm">/* Ocultar información añadida por amazon antes de almacenar */</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Server</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">x</span><span class="o">-</span><span class="n">amz</span><span class="o">-</span><span class="n">id</span><span class="o">-</span><span class="mi">2</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">x</span><span class="o">-</span><span class="n">amz</span><span class="o">-</span><span class="n">request</span><span class="o">-</span><span class="n">id</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">x</span><span class="o">-</span><span class="n">amz</span><span class="o">-</span><span class="n">version</span><span class="o">-</span><span class="n">id</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">beresp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">ETag</span><span class="p">;</span>
<span class="k">return</span> <span class="p">(</span><span class="n">deliver</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">sub</span> <span class="n">vcl_deliver</span> <span class="p">{</span>
<span class="cm">/* Eliminar informacion superflua antes de enviar respuesta */</span>
<span class="n">unset</span> <span class="n">resp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">Via</span><span class="p">;</span>
<span class="n">unset</span> <span class="n">resp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">X</span><span class="o">-</span><span class="n">Varnish</span><span class="p">;</span>
<span class="cm">/* DEBUG: Util para comprobar el correcto funcionamiento de la cache */</span>
<span class="cm">/* unset resp.http.Age; */</span>
<span class="k">if</span> <span class="p">(</span><span class="n">obj</span><span class="p">.</span><span class="n">hits</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">set</span> <span class="n">resp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">X</span><span class="o">-</span><span class="n">Cache</span> <span class="o">=</span> <span class="s">"HIT"</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">set</span> <span class="n">resp</span><span class="p">.</span><span class="n">http</span><span class="p">.</span><span class="n">X</span><span class="o">-</span><span class="n">Cache</span> <span class="o">=</span> <span class="s">"MISS"</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="p">(</span><span class="n">deliver</span><span class="p">);</span>
<span class="p">}</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/varnish-configuracion-s3.vcl">Descargar</a></p>
<p>El archivo de configuración del demonio es <strong>/etc/default/varnish</strong>, y en el
hay que modificar el puerto de escucha y especificar como queremos que
<strong>Varnish</strong> almacene la cache, esto dependerá de la cantidad de memoria
disponible, el tamaño del <em>bucket</em>, y la latencia del disco duro. En mi
caso el servidor es un VPS con 512MB de RAM y una SSD de 20GB alojado en
<a class="reference external" href="http://www.digitalocean.com/">Digital Ocean</a>, y como no tiene suficiente
RAM, uso un archivo para la cache:</p>
<div class="highlight"><pre><span></span><span class="nv">DAEMON_OPTS</span><span class="o">=</span><span class="s2">"-a :80 \</span>
<span class="s2"> -T localhost:6082 \</span>
<span class="s2"> -f /etc/varnish/default.vcl \</span>
<span class="s2"> -S /etc/varnish/secret \</span>
<span class="s2"> -s file,/var/lib/varnish/</span><span class="nv">$INSTANCE</span><span class="s2">/varnish_storage.bin,4G"</span>
</pre></div>
<p>Si tienes disponible suficiente memoria para almacenar la cache en RAM, podrías
usar la configuración:</p>
<div class="highlight"><pre><span></span><span class="nv">DAEMON_OPTS</span><span class="o">=</span><span class="s2">"-a :80 \</span>
<span class="s2"> -T localhost:6082 \</span>
<span class="s2"> -f /etc/varnish/default.vcl \</span>
<span class="s2"> -S /etc/varnish/secret \</span>
<span class="s2"> -s malloc,4G"</span>
</pre></div>
<p>Una vez configurado y reiniciado, puedes comprobar que esta funcionando
correctamente con curl.
Al descargar cualquier documento por primera vez debería contener
<em>X-Cache: MISS</em>, sucesivas peticiones del mismo documento contendrán
<em>X-Cache: HIT</em> si la cache esta funcionando.</p>
<div class="highlight"><pre><span></span>$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, <span class="m">17</span> Mar <span class="m">2014</span> <span class="m">17</span>:51:08 GMT
Content-Type: image/jpeg
Expires: <span class="m">604800</span>.000
Cache-Control: max-age<span class="o">=</span><span class="m">604800</span>
Content-Length: <span class="m">9920</span>
Accept-Ranges: bytes
Date: Sat, <span class="m">22</span> Mar <span class="m">2014</span> <span class="m">02</span>:55:40 GMT
Age: <span class="m">0</span>
Connection: keep-alive
X-Cache: MISS
$ curl -I static.tudominio.com/imagen.jpg
Last-Modified: Mon, <span class="m">17</span> Mar <span class="m">2014</span> <span class="m">17</span>:51:08 GMT
Content-Type: image/jpeg
Expires: <span class="m">604800</span>.000
Cache-Control: max-age<span class="o">=</span><span class="m">604800</span>
Content-Length: <span class="m">9920</span>
Accept-Ranges: bytes
Date: Sat, <span class="m">22</span> Mar <span class="m">2014</span> <span class="m">02</span>:55:40 GMT
Age: <span class="m">8</span>
Connection: keep-alive
X-Cache: HIT
</pre></div>
<p>Cuando compruebes que funciona ya puedes comentar la sección que añade el campo
<em>X-Cache</em>.</p>
</div>
<div class="section" id="configurar-django">
<h2>Configurar Django</h2>
<p>Si usas Django, una vez Varnish este funcionando hay que indicar a Storage,
como generar la dirección para los archivos que antes se accedían a traves de
s3.amazoaws.com. El parámetro <strong>AWS_S3_CUSTOM_DOMAIN</strong> permite apuntar al
dominio que estés usando para tu servidor Varnish.</p>
<div class="highlight"><pre><span></span><span class="n">AWS_S3_CUSTOM_DOMAIN</span> <span class="o">=</span> <span class="s1">'servidorvarnish.tudominio.com'</span>
</pre></div>
<p>Si no estás usando el puerto 80 puedes espeficicarlo con:</p>
<div class="highlight"><pre><span></span><span class="n">AWS_S3_CUSTOM_DOMAIN</span> <span class="o">=</span> <span class="s1">'www.tudominio.com:8080'</span>
</pre></div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="https://www.varnish-software.com/static/book/">The Varnish Book</a></li>
<li><a class="reference external" href="http://en.wikipedia.org/wiki/List_of_HTTP_header_fields">List of HTTP header fields (Wikipedia)</a></li>
<li><a class="reference external" href="https://www.varnish-cache.org/docs/3.0/reference/vcl.html">VCL Reference</a></li>
</ul>
</div>
Web crawler con Scrapy2014-03-18T23:33:00+01:002014-03-18T23:33:00+01:00SecNottag:secnot.com,2014-03-18:/web-crawler-con-scrapy.html<p class="first last">Introducción a Scrapy y como usarlo para extraer datos de una página web.</p>
<p>Los programas que recopilan información de una página web en busca de
información se llaman <strong>crawler</strong>, <strong>spider</strong>, o <strong>arañas</strong> en castellano.
Es una tarea tan común, que existen multitud de bibliotecas y frameworks con
este propósito, en algunos casos simplificando la programación hasta el punto
en el que se puede programar un crawler sencillo con menos de 100 lineas de
código. Y eso es justamente lo que voy a hacer en este post.</p>
<p>Imaginemos que quiero crear una pequeña base de datos para organizar que
libros he leído, y cuales me faltan por leer, pero no quiero tener que
introducir manualmente la información de cada libro, así que voy a obtener
toda la informacón de una página web, por ejemplo
<a class="reference external" href="http://www.epublibre.org/">epublibre</a>.</p>
<p>Para programar la araña me he decantado por Python, y un framework llamado
<a class="reference external" href="http://scrapy.org">Scrapy</a> del que he leido mucho últimamente.</p>
<div class="section" id="instalacion">
<h2>Instalación</h2>
<p>La instalación de un paquete en python suele ser muy sencilla, pero en este
caso he tenido algún problema con las dependencias, y he necesitado instalar
algunos paquetes en el sistema:</p>
<div class="highlight"><pre><span></span>$ apt-get install libxml2-dev libxslt-dev libffi-dev
</pre></div>
<p>Tras esto se instala scrapy con pip:</p>
<div class="highlight"><pre><span></span>$ pip install scrapy
</pre></div>
</div>
<div class="section" id="uso">
<h2>Uso</h2>
<p>El primer paso es iniciar un nuevo proyecto:</p>
<div class="highlight"><pre><span></span>$ scrapy startproject epublibre_crawler
</pre></div>
<p>Esto crea un directorio de proyecto con la siguiente estructura:</p>
<div class="highlight"><pre><span></span>epublibre_crawler/
├── epublibre_crawler
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
</pre></div>
<p>Una vez creado el proyecto, tenemos que definir los items que queremos extraer,
o mejor dicho la clase donde se almacenaran los datos extraídos por
scrapy. En mi caso el titulo del libro, y su autor es suficiente, así que
añadimos al archivo <strong>items.py</strong> la subclase de Item:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">scrapy.item</span> <span class="kn">import</span> <span class="n">Item</span><span class="p">,</span> <span class="n">Field</span>
<span class="k">class</span> <span class="nc">BookItem</span><span class="p">(</span><span class="n">Item</span><span class="p">):</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">Field</span><span class="p">()</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">Field</span><span class="p">()</span>
</pre></div>
<p>El siguiente paso es describir usando expresiones XPath, como se puede extraer
la información del titulo y autor, de manera que Scrapy pueda diferenciarla
del resto de código html de la página de cada libro.</p>
<p>Si miramos el código html de la página de un libro, el título está contenido
en un <em>div</em> de la forma:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"det_titulo"</span> <span class="na">id</span><span class="o">=</span><span class="s">"titulo_libro"</span> <span class="na">style</span><span class="o">=</span><span class="s">"display:inline-block;"</span><span class="p">></span>
Omega
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>Dado que el titulo tiene un identificador único "titulo_libro", es fácil de
extraer:</p>
<div class="highlight"><pre><span></span><span class="n">xpath</span><span class="p">(</span><span class="s2">"//div[@id='titulo_libro']/text()[normalize-space()]"</span><span class="p">)</span>
</pre></div>
<p>para el nombre del autor, tenemos:</p>
<div class="highlight"><pre><span></span><span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"negrita aut_sec"</span> <span class="na">style</span><span class="o">=</span><span class="s">"display:inline-block;"</span><span class="p">></span>
<span class="p"><</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">"http://www.epublibre.org/autor/index/425"</span><span class="p">></span>Jack McDevitt<span class="p"></</span><span class="nt">a</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>este es el único div de la página que usa la clase <strong>aut_sec</strong>, así que podemos
extraer el nombre con:</p>
<div class="highlight"><pre><span></span><span class="n">xpath</span><span class="p">(</span><span class="s2">"//div[@class='negrita aut_sec']/a/text()"</span><span class="p">)</span>
</pre></div>
<p>Una vez tenemos las reglas de extracción, hay que describir como son las
direcciones de las páginas de libro a las que queremos aplicarlas. El formato
es <em>http://www.epublibre.org/libro/detalle/6467</em>, con el numero final variando
para cada libro:</p>
<div class="highlight"><pre><span></span><span class="s1">'libro/detalle/\d+'</span>
</pre></div>
<p>Por último hay que crear la araña que usara scrapy en el directorio spiders:</p>
<div class="highlight"><pre><span></span><span class="c1">#epublibre_spider.py</span>
<span class="kn">from</span> <span class="nn">scrapy.contrib.spiders</span> <span class="kn">import</span> <span class="n">CrawlSpider</span><span class="p">,</span> <span class="n">Rule</span>
<span class="kn">from</span> <span class="nn">scrapy.contrib.linkextractors.sgml</span> <span class="kn">import</span> <span class="n">SgmlLinkExtractor</span>
<span class="kn">from</span> <span class="nn">scrapy.selector</span> <span class="kn">import</span> <span class="n">Selector</span>
<span class="c1"># Importar la clase donde almacenar los resultados</span>
<span class="kn">from</span> <span class="nn">epublibre_crawler.items</span> <span class="kn">import</span> <span class="n">BookItem</span>
<span class="k">class</span> <span class="nc">BookSpider</span><span class="p">(</span><span class="n">CrawlSpider</span><span class="p">):</span>
<span class="c1"># Nombre de la araña.</span>
<span class="n">name</span> <span class="o">=</span> <span class="s1">'epublibre'</span>
<span class="c1"># Dominios en los que el crawler tiene permiso a acceder</span>
<span class="n">allowed_domains</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'epublibre.org'</span><span class="p">]</span>
<span class="c1"># La direccion de inicio para el crawler</span>
<span class="n">start_urls</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'http://www.epublibre.org'</span><span class="p">]</span>
<span class="c1"># Regla para diferenciar los enlaces de libros y función que se les aplica</span>
<span class="n">rules</span> <span class="o">=</span> <span class="p">[</span><span class="n">Rule</span><span class="p">(</span><span class="n">SgmlLinkExtractor</span><span class="p">(</span><span class="n">allow</span><span class="o">=</span><span class="p">[</span><span class="s1">'/libro/detalle/\d+'</span><span class="p">]),</span> <span class="s1">'parse_book'</span><span class="p">)]</span>
<span class="k">def</span> <span class="nf">parse_book</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">response</span><span class="p">):</span>
<span class="sd">""" Parser para las pagina de detalle de los libros"""</span>
<span class="n">sel</span> <span class="o">=</span> <span class="n">Selector</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
<span class="c1"># Creamos un nuevo libro y asignamos los valores extraidos a</span>
<span class="c1"># los campos correspondientes.</span>
<span class="n">book</span> <span class="o">=</span> <span class="n">BookItem</span><span class="p">()</span>
<span class="n">author</span> <span class="o">=</span> <span class="n">sel</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s2">"//div[@class='negrita aut_sec']/a/text()"</span><span class="p">)</span><span class="o">.</span><span class="n">extract</span><span class="p">()</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">sel</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s2">"//div[@id='titulo_libro']/text()[normalize-space()]"</span><span class="p">)</span><span class="o">.</span><span class="n">extract</span><span class="p">()</span>
<span class="c1"># Con Strip eliminamos tabulaciones y linea nueva.</span>
<span class="n">book</span><span class="p">[</span><span class="s1">'title'</span><span class="p">]</span> <span class="o">=</span> <span class="n">title</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">"</span><span class="se">\t\n\r</span><span class="s2">"</span><span class="p">)</span>
<span class="n">book</span><span class="p">[</span><span class="s1">'author'</span><span class="p">]</span> <span class="o">=</span> <span class="n">author</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">"</span><span class="se">\t\n\r</span><span class="s2">"</span><span class="p">)</span>
<span class="k">return</span> <span class="n">book</span>
</pre></div>
<p>Antes de iniciar scrapy hay que modificar las configuración y limitar la
velocidad a la que los datos son accedidos, para no crear un ataque DOS:</p>
<div class="highlight"><pre><span></span><span class="c1"># Scrapy settings for epublibre_scrapy project</span>
<span class="c1">#</span>
<span class="c1"># For simplicity, this file contains only the most important settings by</span>
<span class="c1"># default. All the other settings are documented here:</span>
<span class="c1">#</span>
<span class="c1"># http://doc.scrapy.org/en/latest/topics/settings.html</span>
<span class="c1">#</span>
<span class="n">BOT_NAME</span> <span class="o">=</span> <span class="s1">'epublibre_crawler'</span>
<span class="n">SPIDER_MODULES</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'epublibre_scrapy.spiders'</span><span class="p">]</span>
<span class="n">NEWSPIDER_MODULE</span> <span class="o">=</span> <span class="s1">'epublibre_scrapy.spiders'</span>
<span class="c1"># Maximo una pagina cada minuto</span>
<span class="n">DOWNLOAD_DELAY</span> <span class="o">=</span> <span class="mi">60</span>
<span class="c1"># Identificate</span>
<span class="c1"># USER_AGENT = 'epublibre_crawler (+http://www.tudominio.com)'</span>
</pre></div>
<p>Finalmente solo queda ejecutar scrapy para que recolecte la información y la
guarde en un archivo en alguno de los formatos soportados (XML, JSON, CSV),
o hasta directamente en una base de datos usando una <em>pipeline</em>. En mi
caso JSON es el formato elegido:</p>
<div class="highlight"><pre><span></span>$ scrapy crawl epublibre -o libros.json -t json
</pre></div>
<p>el nombre <strong>epublibre</strong> es el que asigné a la araña BookSpider en la
variable name.</p>
<p>El formato del archivo de salida es el siguiente:</p>
<div class="highlight"><pre><span></span><span class="p">[{</span><span class="nt">"author"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Ray Bradbury"</span><span class="p">],</span> <span class="nt">"title"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Fahrenheit 451"</span><span class="p">]},</span>
<span class="p">{</span><span class="nt">"author"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"John Scalzi"</span><span class="p">],</span> <span class="nt">"title"</span><span class="p">:</span> <span class="p">[</span><span class="s2">"Redshirts"</span><span class="p">]}</span>
<span class="p">]</span>
</pre></div>
<p><em>Nota: Este programa es sólo un ejemplo para explicar el funcionamiento de
scrapy, si realmente deseas extraer la información de epublibre.org, la propia
página distribuye un archivo CSV con todos los datos de los libros
disponibles.</em></p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://doc.scrapy.org/en/latest/">Documentación Scrapy</a></li>
<li><a class="reference external" href="http://www.w3.org/TR/xpath">Leguaje XPatch</a></li>
</ul>
</div>
Introducción a easy_thumbnails2014-02-26T12:31:00+01:002014-02-26T12:31:00+01:00SecNottag:secnot.com,2014-02-26:/introduccion-easy-thumbnails.html<p class="first last">Tutorial sobre el funcionamiento y uso de easy_thumbails.</p>
<p>Una situación muy común en cualquier aplicación web es la necesidad de mostrar
miniaturas de las imágenes subidas por los usuarios. Esto se puede conseguir
usando CSS para escalar la imagen, cortarla, y hasta convertirla a blanco y
negro, pero es un desperdicio de ancho de banda.</p>
<p>En Django existen varias <em>apps</em> para simplificar la tarea, pero mi preferida
por su simplicidad y facilidad de integración es
<a class="reference external" href="https://github.com/SmileyChris/easy-thumbnails">easy-thumbnails</a></p>
<div class="section" id="instalacion">
<h2>Instalación</h2>
<p>Usa pip para instalar en paquete:</p>
<div class="highlight"><pre><span></span>$ pip install easy_thumbnails
</pre></div>
<p>Añade easy_thumbnails a la lista de aplicaciones instaladas en el archivo
<strong>settings.py</strong>:</p>
<div class="highlight"><pre><span></span><span class="n">INSTALLED_APPS</span> <span class="o">=</span> <span class="p">(</span>
<span class="o">...</span>
<span class="s1">'easy_thumbnails'</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
<div class="section" id="uso">
<h2>Uso</h2>
<p>Easy-thumbnails funciona generando dinámicamente miniaturas desde las imágenes
originales. Cuando llega una petición y la miniatura no existe, o ha sido
modificada, una nueva miniatura es generada y salvada.</p>
<p>Para mostrar las miniaturas en los <em>templates</em>, es necesario usar los tags
proporcionados por easy_thumbnails, <strong>thumbnail</strong> y <strong>thumbnail-url</strong>:</p>
<div class="highlight"><pre><span></span>{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"producto"</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% thumbnail producto.foto 90x90 crop %}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>{{ producto.nombre }}<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>En el tag indicamos la imagen fuente, el tamaño al que deseamos convertir la
imagen, seguido de cualquier opción adicional, en este caso indicamos que corte
la imagen si es necesario para llegar al tamaño seleccionado.</p>
<p>También es posible indicar solo una de las dimensiones de la imagen, de manera
que la otra se escale para mantener las proporciones, sin deformar la imagen ni cortar los bordes.</p>
<div class="highlight"><pre><span></span>{% load thumbnail %}
....
<span class="p"><</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">"producto"</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% thumbnail producto.foto 90x0 %}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>{{ producto.nombre }}<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>Para simplificar la tarea y evitar errores, podemos crear un alias para las
distintas configuraciones de las miniaturas, para ello hay que crear un
diccionario llamado THUMBNAIL_ALIASES en <strong>settings.py</strong>, que contiene un las
opciones de cada alias:</p>
<div class="highlight"><pre><span></span><span class="n">THUMBNAIL_ALIASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">''</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'producto'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'size'</span><span class="p">:</span> <span class="p">(</span><span class="mi">90</span><span class="p">,</span> <span class="mi">90</span><span class="p">),</span> <span class="s1">'crop'</span><span class="p">:</span> <span class="kc">True</span><span class="p">},</span>
<span class="s1">'cartel'</span><span class="p">:</span> <span class="p">{</span><span class="s1">'size'</span><span class="p">:</span> <span class="p">(</span><span class="mi">90</span><span class="p">,</span> <span class="mi">0</span><span class="p">),},</span>
<span class="p">},</span>
<span class="p">}</span>
</pre></div>
<p>Luego podemos usar estos alias con:</p>
<div class="highlight"><pre><span></span>{# Cargamos los tags de easy_thumbnails #}
{% load thumbnail %}
....
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"{% producto.foto|thumbnail_url:'cartel' %}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>{{ producto.nombre }}<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<div class="section" id="thumbnailerimagefield">
<h3>ThumbnailerImageField</h3>
<p>Este campo nos hace aún más fácil manejar las direcciones de las miniaturas en
en <em>template</em>, y permite gestionar cuando son generadas. Para usarlo solo es
necesario substituir en los modelos ImageField por ThumbnailerImageField:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">easy_thumbnails.fields</span> <span class="kn">import</span> <span class="n">ThumbnailerImageField</span>
<span class="k">class</span> <span class="nc">Producto</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="sd">"""Mi modelo de un producto"""</span>
<span class="n">foto</span> <span class="o">=</span> <span class="n">ThumbnailerImageField</span><span class="p">()</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</pre></div>
<p>En los <em>templates</em> puedes obtener la dirección de la miniatura fácilmente
usado su alias:</p>
<div class="highlight"><pre><span></span>{% load thumbnail %}
....
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">"{{ producto.foto.cartel.url }}"</span><span class="p">/></span>
<span class="p"><</span><span class="nt">h3</span><span class="p">></span>{{ producto.nombre }}<span class="p"></</span><span class="nt">h3</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
</pre></div>
<p>Si la aplicación requiere que las miniaturas sean generadas en el momento que
las imágenes son subidas, se pueden usar los manejadores de señales en
<strong>models.py</strong>:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">easy_thumbnails.signals</span> <span class="kn">import</span> <span class="n">saved_file</span>
<span class="kn">from</span> <span class="nn">easy_thumbnails.signal_handlers</span> <span class="kn">import</span> <span class="n">generate_aliases_global</span>
<span class="n">saved_file</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">generate_aliases_global</span><span class="p">)</span>
</pre></div>
</div>
</div>
<div class="section" id="integracion-con-amazon-s3">
<h2>Integración con Amazon S3</h2>
<p>En caso de que necesites usar <a class="reference external" href="aws.amazon.con/s3/">Amazon S3</a> para
almacenar imágenes, ya sea para servirlas directamente, o como backup,
configurar easy_thumbnails es muy sencillo.</p>
<p>Primero necesitamos instalar django-storage y la biblioteca boto de python para
S3.</p>
<div class="highlight"><pre><span></span>$ pip install django-storage
$ pip install boto
</pre></div>
<p>Una vez instalados configuramos easy_thumbnails para indicarle que tiene que
usar S3 como almacenamiento, en <strong>settings.py</strong> hay que añadir:</p>
<div class="highlight"><pre><span></span><span class="n">THUMBNAIL_DEFAULT_STORAGE</span> <span class="o">=</span><span class="s1">'storages.backends.s3boto.S3BotoStorage'</span>
<span class="n">THUMBNAIL_BASEDIR</span> <span class="o">=</span> <span class="s1">'_miniaturas/'</span>
</pre></div>
<p>Ademas de indicar que use S3, lo he configurado para que guarde las miniaturas
en un directorio aparte llamado '_miniaturas/', esto hace que sean más
sencillas de gestionar.</p>
<p>Tras esto configuramos Django para que también guarde la imagen original en S3,
añadimos las credenciales AWS, y el nombre del <em>bucket</em> a usar:</p>
<div class="highlight"><pre><span></span><span class="n">DEFAULT_FILE_STORAGE</span> <span class="o">=</span> <span class="s1">'storages.backends.s3boto.S3BotoStorage'</span>
<span class="n">AWS_S3_SECURE_URLS</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># Usar http en lugar de https</span>
<span class="n">AWS_QUERYSTRING_AUTH</span> <span class="o">=</span> <span class="kc">False</span> <span class="c1"># authentication sencilla</span>
<span class="n">AWS_ACCESS_KEY_ID</span> <span class="o">=</span> <span class="s1">'Tu_ACCESS_KEY_ID'</span>
<span class="n">AWS_SECRET_ACCESS_KEY</span> <span class="o">=</span> <span class="s1">'Tu_SECRET_ACCESS_KEY'</span>
<span class="n">AWS_STORAGE_BUCKET_NAME</span> <span class="o">=</span> <span class="s1">'nombre_bucket'</span>
</pre></div>
<p>Si usas S3 es recomendable usar señales para que easy_thumbnails calcule las
miniaturas en el momento de creación de las imágenes.</p>
<p>Por último sincronizamos la base de datos</p>
<div class="highlight"><pre><span></span>$ python manage.py syncdb
</pre></div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://easy-thumbnails.readthedocs.org/en/latest/">Documentación de easy_thumbnails</a></li>
<li><a class="reference external" href="https://github.com/mariocesar/sorl-thumbnail">Sorl thumbnails</a></li>
</ul>
</div>
Tests más rápidos en Django con SQLite2014-02-15T22:27:00+01:002014-02-15T22:27:00+01:00SecNottag:secnot.com,2014-02-15:/tests-django-rapidos-con-sqlite.html<p class="first last">Como usar SQLite para acelerar los tests en Django.</p>
<p>Esta semana he estado desarrollando una webapp django en un ordenador sin SSD,
y el tiempo de ejecución era mucho mayor de lo normal, ya no recordaba lo
lentos que son los discos duros especialmente su acceso aleatorio.
Por supuesto me he puesto a investigar formas de acelerar el proceso.</p>
<p>Cuando estás desarrollando una aplicación django tienes tres opciones
como base de datos <a class="reference external" href="www.mysql.com">MySQL</a>,
<a class="reference external" href="www.postgresql.org/">PostgreSQL</a>, o <a class="reference external" href="http://www.sqlite.org/">SQLite</a>.
A la hora de hacer tests SQLite es indiscutiblemente la más rápida, ya que
crea la base de datos en ram, pero no tiene soporte completo de ALTER TABLE,
por lo que no es posible hacer migraciones. MySQL y PostgreSQL en cambio
son lentas en la creación de bases de datos, pero soportan migraciones.</p>
<p>La solución perfecta es usar MySQL o PostgreSQL para el desarrollo, y usar
SQLite en los tests. Esto lo podemos conseguir modificando el archivo de
configuración cuando se van a ejecutar tests, para cambiar la base de datos a
sqlite3.</p>
<p>Primero creamos un archivo de configuración alternativo <strong>test_settings.py</strong>,
en el añadimos la configuración de la base de datos a usar en los tests:</p>
<div class="highlight"><pre><span></span><span class="c1"># test_settings.py</span>
<span class="n">DATABASES</span> <span class="o">=</span> <span class="p">{</span>
<span class="s1">'default'</span><span class="p">:</span> <span class="p">{</span>
<span class="s1">'ENGINE'</span><span class="p">:</span> <span class="s1">'django.db.backends.sqlite3'</span><span class="p">,</span> <span class="c1">#</span>
<span class="s1">'NAME'</span><span class="p">:</span> <span class="s1">'test_sqlitedb'</span><span class="p">,</span> <span class="c1"># Ruta al archivo de la base de datos</span>
<span class="p">}</span>
<span class="p">}</span>
</pre></div>
<p>Tras esto solo queda modificar settings.py para que cargue el archivo de
configuración cuando esté ejecutando tests, esto lo conseguimos comprobando si
los argumentos de la linea de comandos contiene la palabra "test". Al final
del archivo <strong>settings.py</strong> hay que añadir:</p>
<div class="highlight"><pre><span></span><span class="c1"># ^^^^^^^^ Resto de configuracion settings.py ^^^^^^</span>
<span class="c1"># Cargar configuracion test_settings.py si se ejecuta manage.py test</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="k">if</span> <span class="s1">'test'</span> <span class="ow">in</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="kn">from</span> <span class="nn">test_settings</span> <span class="kn">import</span> <span class="o">*</span>
<span class="k">except</span> <span class="ne">ImportError</span><span class="p">:</span>
<span class="nb">print</span> <span class="s2">"No se pudo encontrar el archivo test_settings.py"</span>
</pre></div>
<p>El tiempo de ejecución de la aplicación usando PostgreSQL:</p>
<div class="highlight"><pre><span></span>$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 1.047s
OK
Destroying test database for alias 'default'...
real 0m14.169s
user 0m1.204s
sys 0m0.136s
</pre></div>
<p>Y usando SQLite:</p>
<div class="highlight"><pre><span></span>$ time python manage.py test nombre_app
Creating test database for alias 'default'...
...............
----------------------------------------------------------------------
Ran 15 tests in 0.299s
OK
Destroying test database for alias 'default'...
real 0m1.031s
user 0m0.872s
sys 0m0.100s
</pre></div>
<p>Una reducción del 90% (13 segundos), nada mal para una modificación tan sencilla.</p>
Problemas con ImageField en Django2014-02-11T15:27:00+01:002014-02-11T15:27:00+01:00SecNottag:secnot.com,2014-02-11:/problemas-con-imagefield-en-django.html<p class="first last">Soluciones a los problemas más comunes del campo ImageField.</p>
<p>ImageField proporciona una buena abstracción para la gestión imágenes,
hace fácil la subida de los archivos a la pagina, y proporciona tags para
mostrarlas en los <em>template</em>.
Pero ciertos comportamientos por defecto del campo, hacen imposible usarlo sin
modificaciones en prácticamente cualquier proyecto django.</p>
<p>Estas son algunas de las modificaciones que suelo hacer en casi cualquier
modelo con un campo ImageField.</p>
<div class="section" id="cambiar-el-nombre-del-archivo-antes-de-guardarlo">
<h2>Cambiar el nombre del archivo antes de guardarlo</h2>
<p>Cuando ImageField salva la imagen, lo hace en un archivo con el mismo nombre
que el archivo original, esto es un problema puesto que si se salvan dos
imágenes con el mismo nombre una sobreescribe a la otra.</p>
<p>Este comportamiento es admisible un blog, donde el autor puede controlar
el nombre de cada imagen, en cambio en una página donde los usuarios puedan
subir imágenes, no tardaría en ser un problema grave.</p>
<p>La solución es usar el parámetro de ImageField <strong>upload_to</strong>, para
proporcionar un nombre de archivo aleatorio con el que salvar la imagen.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">uuid</span> <span class="kn">import</span> <span class="n">uuid4</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">date</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">_generar_ruta_imagen</span><span class="p">(</span><span class="n">instance</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<span class="c1"># El primer paso es extraer la extension de la imagen del</span>
<span class="c1"># archivo original</span>
<span class="n">extension</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">filename</span><span class="p">)[</span><span class="mi">1</span><span class="p">][</span><span class="mi">1</span><span class="p">:]</span>
<span class="c1"># Generamos la ruta relativa a MEDIA_ROOT donde almacenar</span>
<span class="c1"># el archivo, usando la fecha actual (año/mes)</span>
<span class="n">ruta</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s1">'Imagenes'</span><span class="p">,</span> <span class="n">date</span><span class="o">.</span><span class="n">today</span><span class="p">()</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y/%m"</span><span class="p">))</span>
<span class="c1"># Generamos el nombre del archivo con un identificador</span>
<span class="c1"># aleatorio, y la extension del archivo original.</span>
<span class="n">nombre_archivo</span> <span class="o">=</span> <span class="s1">'</span><span class="si">{}</span><span class="s1">.</span><span class="si">{}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">uuid4</span><span class="p">()</span><span class="o">.</span><span class="n">hex</span><span class="p">,</span> <span class="n">extension</span><span class="p">)</span>
<span class="c1"># Devolvermos la ruta completa</span>
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ruta</span><span class="p">,</span> <span class="n">nombre_archivo</span><span class="p">)</span>
<span class="n">imagen</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ImageField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="n">_generar_ruta_imagen</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_lenght</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
</pre></div>
<p>La función <strong>_generar_ruta_imagen</strong> es llamada la primera vez que se salva la
imagen para generar la ruta.
Si estudias su código, verás que ademas de generar el nombre del archivo,
generamos la ruta donde es almacenado, esta ruta se genera con la fecha en el
momento de salvado, de manera que cambia con el paso del tiempo.
Con esto se evita que miles de imágenes se guarden en un solo directorio,
haciéndolo difícil de listar y manipular.</p>
</div>
<div class="section" id="usar-un-mixin-para-anadir-imagenes-a-varios-modelos">
<h2>Usar un Mixin para añadir imágenes a varios modelos</h2>
<p>En el caso de que tengamos varios modelos que necesiten una imagen, podemos
crear una clase abstracta que proporciona un campo imagen, que genere de
forma correcta las rutas y nombres. Luego solo tenemos que heredar de ella en
cada modelo que necesite esa funcionalidad.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">uuid</span> <span class="kn">import</span> <span class="n">uuid4</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">date</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="k">class</span> <span class="nc">ImagenMixin</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">_generar_ruta_imagen</span><span class="p">(</span><span class="n">instance</span><span class="p">,</span> <span class="n">filename</span><span class="p">):</span>
<span class="c1"># El primer paso es extraer la extension de la imagen del</span>
<span class="c1"># archivo original</span>
<span class="n">extension</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">filename</span><span class="p">)[</span><span class="mi">1</span><span class="p">][</span><span class="mi">1</span><span class="p">:]</span>
<span class="c1"># Generamos la ruta relativa a MEDIA_ROOT donde almacenar</span>
<span class="c1"># el archivo, se usa el nombre de la clase y la fecha actual.</span>
<span class="n">directorio_clase</span> <span class="o">=</span> <span class="n">instance</span><span class="o">.</span><span class="vm">__class__</span><span class="o">.</span><span class="vm">__name__</span>
<span class="n">ruta</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s1">'imagenes'</span><span class="p">,</span> <span class="n">directorio_clase</span><span class="p">,</span>
<span class="n">date</span><span class="o">.</span><span class="n">today</span><span class="p">()</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y/%m"</span><span class="p">))</span>
<span class="c1"># Generamos el nombre del archivo con un identificador</span>
<span class="c1"># aleatorio, y la extension del archivo original.</span>
<span class="n">nombre_archivo</span> <span class="o">=</span> <span class="s1">'</span><span class="si">{}</span><span class="s1">.</span><span class="si">{}</span><span class="s1">'</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">uuid4</span><span class="p">()</span><span class="o">.</span><span class="n">hex</span><span class="p">,</span> <span class="n">extension</span><span class="p">)</span>
<span class="c1"># Devolvermos la ruta completa</span>
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">ruta</span><span class="p">,</span> <span class="n">nombre_archivo</span><span class="p">)</span>
<span class="n">imagen</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ImageField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="n">_generar_ruta_imagen</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Meta</span><span class="p">:</span>
<span class="n">abstract</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">ImagenMixin</span><span class="p">):</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_lenght</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">Libro</span><span class="p">(</span><span class="n">ImagenMixin</span><span class="p">):</span>
<span class="n">titulo</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">100</span><span class="p">)</span>
</pre></div>
<p>El único cambio está en <strong>_generar_ruta_imagen</strong> de la clase abstracta,
donde se añade a la ruta de salvado el nombre de la clase.
De esta manera se separan las imágenes salvadas por cada clase en un
directorio distinto.
Por ejemplo los modelos Post y Libro guardarían sus imágenes en:</p>
<blockquote>
<ul class="simple">
<li><strong>/MEDIA_ROOT/imagenes/Post/año/mes/</strong></li>
<li><strong>/MEDIA_ROOT/imagenes/Libro/año/mes/</strong></li>
</ul>
</blockquote>
</div>
<div class="section" id="mostrar-imagenes-en-el-interfaz-admin">
<h2>Mostrar imágenes en el interfaz Admin</h2>
<p>Si usas admin para añadir contenido a tu página web, es realmente incómodo no
poder ver las imágenes directamente en su interfaz. Para solucionarlo creamos
un método en el modelo, que devuelve un tag <img> apuntando a la dirección de
la imagen:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.utils.safestring</span> <span class="kn">import</span> <span class="n">mark_safe</span>
<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">imagen</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ImageField</span><span class="p">()</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">imagen_admin</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">image</span><span class="p">:</span>
<span class="c1"># Marcamos imagen como safe para evitar escape automatico</span>
<span class="k">return</span> <span class="n">mark_safe</span><span class="p">(</span><span class="sa">u</span><span class="s1">'<img src="</span><span class="si">%s</span><span class="s1">" />'</span> <span class="o">%</span> <span class="bp">self</span><span class="o">.</span><span class="n">imagen</span><span class="o">.</span><span class="n">url</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="s1">'(Sin imagen)'</span>
<span class="c1"># Para cambiar el nombre del campo en pantalla</span>
<span class="n">imagen_admin</span><span class="o">.</span><span class="n">short_description</span> <span class="o">=</span> <span class="s1">'Imagen'</span>
<span class="c1"># En lugar de mark_safe podemos añadir:</span>
<span class="n">imagen_admin</span><span class="o">.</span><span class="n">allow_tags</span> <span class="o">=</span> <span class="kc">True</span>
</pre></div>
<p>Y modificamos el ModelAdmin del modelo para que muestre la imagen. Hay que
asegurarse de añadir el nuevo método a <strong>readonly_fields</strong>:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">blog.models</span> <span class="kn">import</span> <span class="n">Post</span>
<span class="k">class</span> <span class="nc">PostAdmin</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
<span class="c1"># imagen_admin tiene que ser siempre readonly, si queremos modificar</span>
<span class="c1"># la imagen hay que hacerlo a traves del campo imagen.</span>
<span class="n">readonly_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'imagen_admin'</span><span class="p">,)</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'imagen_admin'</span><span class="p">,</span> <span class="s1">'imagen'</span><span class="p">,</span> <span class="s1">'text'</span><span class="p">,)</span>
<span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">Post</span><span class="p">,</span> <span class="n">PostAdmin</span><span class="p">)</span>
</pre></div>
<p>Si no quieres modificar tu modelo, puedes crear un método similar
directamente en ModelAdmin:</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="nn">blog.models</span> <span class="kn">import</span> <span class="n">Post</span>
<span class="k">class</span> <span class="nc">PostAdmin</span><span class="p">(</span><span class="n">admin</span><span class="o">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">imagen_admin</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">obj</span><span class="p">):</span>
<span class="k">return</span> <span class="sa">u</span><span class="s1">'<img src="</span><span class="si">%s</span><span class="s1">" />'</span> <span class="o">%</span> <span class="n">obj</span><span class="o">.</span><span class="n">image</span><span class="o">.</span><span class="n">url</span>
<span class="n">imagen_admin</span><span class="o">.</span><span class="n">allow_tags</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">readonly_fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'imagen_admin'</span><span class="p">,)</span>
<span class="n">fields</span> <span class="o">=</span> <span class="p">(</span><span class="s1">'imagen_admin'</span><span class="p">,</span> <span class="s1">'text'</span><span class="p">)</span>
<span class="n">admin</span><span class="o">.</span><span class="n">site</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">Post</span><span class="p">,</span> <span class="n">PostAdmin</span><span class="p">)</span>
</pre></div>
<p>Por último si estás usando el mixin de la sección anterior, ese es el mejor
lugar donde añadir el método.</p>
</div>
<div class="section" id="eliminar-el-archivo-de-la-imagen-al-borrar-el-modelo">
<h2>Eliminar el archivo de la imagen al borrar el modelo</h2>
<p>El comportamiento por defecto de ImageField, es no borrar el archivo cuando el
modelo es eliminado, para borrarlo es necesario conectarse a la señal
<em>pre_delete</em> que se envía antes de la eliminación de cualquier modelo, y
hacerlo manualmente.</p>
<div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">django.db</span> <span class="kn">import</span> <span class="n">models</span>
<span class="kn">from</span> <span class="nn">django.db.models.signals</span> <span class="kn">import</span> <span class="n">pre_delete</span><span class="p">,</span> <span class="n">pre_save</span>
<span class="kn">from</span> <span class="nn">django.dispatch</span> <span class="kn">import</span> <span class="n">receiver</span>
<span class="k">class</span> <span class="nc">Post</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span>
<span class="n">imagen</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">ImageField</span><span class="p">()</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">models</span><span class="o">.</span><span class="n">TextField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">1000</span><span class="p">)</span>
<span class="c1"># Usamos el Decorador receiver para ejecutar nuestra función</span>
<span class="c1"># cuando el Post el borrado.</span>
<span class="nd">@receiver</span><span class="p">(</span><span class="n">pre_delete</span><span class="p">,</span> <span class="n">sender</span><span class="o">=</span><span class="n">Post</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">post_pre_delete_handler</span><span class="p">(</span><span class="n">sender</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
<span class="n">instance</span><span class="o">.</span><span class="n">imagen</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="kc">False</span><span class="p">)</span>
</pre></div>
<p>Ten en cuenta que si borras el archivo con el modelo, puedes perder datos
si hay algún accidente, si el numero de imágenes almacenado no es grande
es mejor no borrarlas. Si el número es grande, puedes usar una tarea asíncrona
que borre los archivos huérfanos tras borrar los modelos, esto lo puedes
conseguir con celery, cron, o similar.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="https://docs.djangoproject.com/en/dev/ref/models/fields/">Django model reference</a></li>
</ul>
</div>
Desplegar aplicaciones Django con Nginx y Gunicorn2014-02-08T21:33:00+01:002014-02-08T21:33:00+01:00SecNottag:secnot.com,2014-02-08:/django-nginx-gunicorn.html<p class="first last">Aprende a desplegar aplicaciones django usando gunicorn y nginx.</p>
<p>Si estás desarrollando una aplicación web, algún día tendrás que dar el paso
y hacerla pública. Inicialmente puedes usar un servicio como
<a class="reference external" href="https://www.heroku.com">Heroku</a> pero si tienes éxito, no te quedará mas
remedio que gestionar tus propios servidores o VPS.</p>
<p>Por fortuna desplegar una aplicación django usando nginx y gunicorn, es
más sencillo de lo que podría parecer. En esta pequeña guia trato de describir
el proceso paso a paso:</p>
<div class="section" id="crear-un-usuario-para-la-aplicacion-django">
<h2>1 - Crear un usuario para la aplicación django</h2>
<p>El primer paso es crear un usuario con el que ejecutar la aplicación
Django, esto nos permite organizar con facilidad un servidor donde estemos
ejecutando varias aplicaciones, proporciona separación de privilegios, y limita
el posible daño que pueda hacerse al sistema si la aplicación es comprometida.</p>
<p>Creamos un grupo al que pertenecerán todos los usuarios de las aplicaciones
django, y un usuario al que le asignamos el nombre de la aplicación.</p>
<div class="highlight"><pre><span></span>$ sudo addgroup --system webapps
$ sudo adduser --system --ingroup webapps --home /webapps/appname appname
</pre></div>
<p>Al usar en modificador <em>--system</em>, al usuario se le asigna /bin/false como
shell, y no tiene clave, por lo que no puede hacer login. Aunque esto es
una buena medida de seguridad, es muy incomodo mientras se está configurando
el sistema, así que asignamos un shell temporalmente:</p>
<div class="highlight"><pre><span></span>$ sudo chsh -s /bin/bash appname
</pre></div>
<p>Esto nos permite usar <strong>sudo su appname</strong> para seguir con la instalación como
el nuevo usuario.</p>
</div>
<div class="section" id="instalar-y-configurar-virtualenv">
<h2>2 - Instalar y configurar virtualenv</h2>
<p>Virtualenv es la herramienta que nos permite aislar los paquetes requeridos
por las aplicaciones, de manera que si dos aplicaciones necesitan paquetes
que están en conflicto, no interfieran la una con la otra como ocurriría si
instalásemos todos los paquetes directamente en el sistema.
Instalamos virtualenv con:</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install python-virtualenv python-pip
</pre></div>
<p>Una vez instalado, hacemos login con el usuario que hemos creado, y dentro de
su directorio usamos virtualenv para generar un nuevo entorno virtual:</p>
<div class="highlight"><pre><span></span>$ sudo su appname
$ <span class="nb">cd</span>
$ mkdir virtualenvs
$ virtualenv --no-site-packages virtualenvs/app_env
$ <span class="nb">source</span> virtualenvs/app_env/bin/activate
</pre></div>
<p>Dentro del entorno, hay que instalar todos los paquetes que sean necesarios
para la aplicación, puedes hacerlo uno a uno, o en bloque si tienes el archivo
requirements.txt:</p>
<div class="highlight"><pre><span></span><span class="o">(</span>app_env<span class="o">)</span>$ pip install django
<span class="o">(</span>app_env<span class="o">)</span>$ pip install django-countries
<span class="o">(</span>app_env<span class="o">)</span>$ pip install django-mptt
<span class="o">(</span>app_env<span class="o">)</span>$ ....
</pre></div>
<div class="highlight"><pre><span></span><span class="o">(</span>app_env<span class="o">)</span>$ pip install -r requirements.txt
</pre></div>
<p>Si quieres profundizar en el funcionamiento de virtualenv sigue este
<a class="reference external" href="https://secnot.com/django-virtualenv.html">tutorial</a></p>
</div>
<div class="section" id="gunicorn-supervisor">
<h2>3 - Gunicorn + Supervisor</h2>
<p>Gunicorn es el servidor WSGI que se encarga de servir la aplicación, pero
necesita un programa que lo inicie al arranque, y lo monitorice para
reiniciarlo si hay algún problema. La mejor solución es usar el gestor de
procesos como <a class="reference external" href="http://supervisord.org/">supervisor</a>.
La instalación es sencilla:</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install supervisor
</pre></div>
<p>Para instalar gunicorn el método más sencillo es hacerlo dentro del entorno
virtual de tu aplicación django.</p>
<div class="highlight"><pre><span></span>$ <span class="nb">source</span> virtualenvs/app_env/bin/activate
<span class="o">(</span>app_env<span class="o">)</span>$ pip install gunicorn
</pre></div>
<p>Una vez todo está instalado, creamos un archivo de configuración para gunicorn
<strong>gunicorn_conf.py</strong> en el directorio HOME del usuario, en el que indicamos la
dirección y puerto en los que estará escuchando gunicorn:</p>
<div class="highlight"><pre><span></span><span class="c1"># gunicorn_conf.py</span>
<span class="n">workers</span> <span class="o">=</span> <span class="mi">3</span>
<span class="n">bind</span> <span class="o">=</span> <span class="s1">'127.0.0.1:9000'</span>
</pre></div>
<p>A partir de aquí ya podemos salir del entorno virtual de la aplicación:</p>
<div class="highlight"><pre><span></span><span class="o">(</span>app_env<span class="o">)</span>$ deactivate
</pre></div>
<p>El siguiente paso es crear el archivo de configuración de supervisor en
<strong>/etc/supervisor/conf.d/appname.conf</strong>:</p>
<div class="highlight"><pre><span></span>[program:appname]
command=/webapps/appname/django_app/run_gunicorn.sh
directory = /webapps/appname/django_app/
user=appname
autostart=true
autorestart=true
priority=991
stopsignal=KILL
</pre></div>
<p>Tras esto creamos en el directorio de la aplicación el script
<strong>run_gunicorn.sh</strong> que etablece el entorno virtualenv para la aplicación y
despuer ejecuta gunicorn:</p>
<div class="highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
<span class="nb">source</span> /webapp/appname/virtualenvs/app_env/bin/activate
<span class="nb">exec</span> /webapp/appname/virtualenvs/app_env/bin/gunicorn -c /webapps/appname/gunicorn_conf.py django_app.wsgi:application
</pre></div>
<p>Una vez configurado indicamos a supervisor que debe iniciar el nuevo servicio:</p>
<div class="highlight"><pre><span></span>$ sudo supervisorctl start reread
appname: available
$ sudo supervisorctl update
appname: added process group
$ sudo supervisorctl status appname
appname RUNNING pid <span class="m">21710</span>, uptime <span class="m">0</span>:00:07
</pre></div>
<p>Para detener e iniciar la aplicación podemos usar:</p>
<div class="highlight"><pre><span></span>$ sudo supervisorctl stop appname
appname: stopped
$ sudo supervisorctl start appname
appname: started
</pre></div>
<p>Llegado a este punto si todo funciona correctamente, ya podemos desactivar
el shell del usuario creado para la aplicación.</p>
<div class="highlight"><pre><span></span>$ sudo -s /bin/false appname
</pre></div>
<p>Si prefieres que sea posible hacer login con el usuario, es recomendable que
le asignes una clave.</p>
</div>
<div class="section" id="nginx">
<h2>4 - Nginx</h2>
<p>Usamos Nginx para hacer de pasarela entre los clientes y gunicorn, y para
servir los archivos estáticos de la aplicación. Para instalarlo:</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install nginx
</pre></div>
<p>Para configurar nginx, hay que crear un archivo <strong>/etc/ngix/sites-available/</strong>,
en el especificaremos donde se debe conectar a gunicorn en la sección upstream,
y la ruta a los archivos estáticos de tu aplicación en la sección server:</p>
<pre class="code nginx literal-block">
<span class="k">upstream</span> <span class="s">django_app_server</span> <span class="p">{</span>
<span class="c1"># Dirección en la que está escuchando gunicorn
</span> <span class="kn">server</span> <span class="n">127.0.0.1</span><span class="p">:</span><span class="mi">9000</span> <span class="s">fail_timeout=0</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">server</span> <span class="p">{</span>
<span class="c1"># listen 80 default deferred; # for Linux
</span> <span class="c1"># listen 80 default accept_filter=httpready; # for FreeBSD
</span> <span class="kn">listen</span> <span class="mi">80</span> <span class="s">default</span><span class="p">;</span>
<span class="kn">client_max_body_size</span> <span class="s">4G</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">www.dominio.com</span><span class="p">;</span>
<span class="c1"># ~2 seconds is often enough for most folks to parse HTML/CSS and
</span> <span class="c1"># retrieve needed images/icons/frames, connections are cheap in
</span> <span class="c1"># nginx so increasing this is generally safe...
</span> <span class="kn">keepalive_timeout</span> <span class="mi">5</span><span class="p">;</span>
<span class="c1"># Ruta a tus archivos estaticos.
</span> <span class="kn">location</span> <span class="s">/static/</span> <span class="p">{</span>
<span class="kn">alias</span> <span class="s">/webapps/appname/django_app/static/</span><span class="p">;</span>
<span class="kn">autoindex</span> <span class="no">on</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
<span class="c1"># an HTTP header important enough to have its own Wikipedia entry:
</span> <span class="c1"># http://en.wikipedia.org/wiki/X-Forwarded-For
</span> <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
<span class="c1"># enable this if and only if you use HTTPS, this helps Rack
</span> <span class="c1"># set the proper protocol for doing redirects:
</span> <span class="c1"># proxy_set_header X-Forwarded-Proto https;
</span>
<span class="c1"># pass the Host: header from the client right along so redirects
</span> <span class="c1"># can be set properly within the Rack application
</span> <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$http_host</span><span class="p">;</span>
<span class="c1"># we don't want nginx trying to do something clever with
</span> <span class="c1"># redirects, we set the Host: header above already.
</span> <span class="kn">proxy_redirect</span> <span class="no">off</span><span class="p">;</span>
<span class="c1"># set "proxy_buffering off" *only* for Rainbows! when doing
</span> <span class="c1"># Comet/long-poll stuff. It's also safe to set if you're
</span> <span class="c1"># using only serving fast clients with Unicorn + nginx.
</span> <span class="c1"># Otherwise you _want_ nginx to buffer responses to slow
</span> <span class="c1"># clients, really.
</span> <span class="c1"># proxy_buffering off;
</span>
<span class="c1"># Try to serve static files from nginx, no point in making an
</span> <span class="c1"># *application* server like Unicorn/Rainbows! serve static files.
</span> <span class="kn">proxy_pass</span> <span class="s">http://django_app_server</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/gunicorn-base-nginx.conf">Descargar</a></p>
<p>Una vez configurado, hay que enlazar el archivo que hemos creado en
<strong>/etc/nginx/sites-available/</strong> a <strong>/etc/nginx/sites-enabled/</strong>, de esta manera
nginx empezará a usar la configuración:</p>
<div class="highlight"><pre><span></span>$ sudo ln -s /etc/nginx/sites-available/midominio /etc/nginx/sites-enabled/midominio
</pre></div>
<p>Por último reiniciamos nginx:</p>
<div class="highlight"><pre><span></span>$ sudo service nginx restart
</pre></div>
<p><em>Nota: Los autores de gunicorn tiene un ejemplo más completo de un archivo de
configuración</em>
<a class="reference external" href="https://raw.github.com/benoitc/gunicorn/master/examples/nginx.conf">nginx.conf</a></p>
</div>
<div class="section" id="alternativas">
<h2>Alternativas</h2>
<p>Por supuesto esta no es la única opción para desplegar django, ni es la mejor
para todas las situaciones. Si estas administrando múltiples aplicaciones,
y/o múltiples servidores quizás <a class="reference external" href="http://www.docker.io/">Docker</a> o
<a class="reference external" href="https://github.com/progrium/dokku">Dokku</a> se ajuste mejor a tus necesidades.
Y como comenté al inicio <a class="reference external" href="https://www.heroku.com">Heroku</a> es una buena
alternativa para páginas con poco tráfico.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://supervisord.org/">Documentación Supervisor</a></li>
<li><a class="reference external" href="http://docs.gunicorn.org/en/latest">Documentación Gunicorn</a></li>
<li><a class="reference external" href="http://nginx.org/en/docs/">Documentación Nginx</a></li>
<li><a class="reference external" href="http://michal.karzynski.pl/blog/2013/06/09/django-nginx-gunicorn-virtualenv-supervisor/">Nginx+Gunicorn+Django Tutorial</a></li>
</ul>
</div>
Django con virtualenv2014-02-02T20:00:00+01:002015-04-14T13:15:00+02:00SecNottag:secnot.com,2014-02-02:/django-virtualenv.html<p class="first last">Tutorial de uso de virtualenv.</p>
<p>Virtualenv es una herramienta para crear entornos virtuales para python.</p>
<p>Cuando mas de una aplicación python esta instalada en un mismo sistema, se
puede dar el caso de que requieran versiones incompatibles de una biblioteca,
que no sea posible instalarla en el directorio site-packages, o que no quieras
actualizar las bibliotecas de la aplicación cuando se actualizen las del
sistema.</p>
<p>Virtualenv soluciona todos estos problemas, permitiendo crear
entornos virtuales en los que se pueden instalar bibliotecas y programas,
de forma independiente del sistema, y otros entornos. Esto es especialmente
útil durante el desarrollo y despliegue de aplicaciones.</p>
<div class="section" id="virtualenv">
<h2>Virtualenv</h2>
<div class="section" id="instalacion">
<h3>Instalación</h3>
<p>Para instalar virtualenv, lo mejor es usar el gestor de paquetes de tu
distribución, para hacer la herramienta disponible a todos los usuarios:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ sudo apt-get install python-virtualenv
</pre></div>
</div>
<div class="section" id="crear-un-entorno">
<h3>Crear un entorno</h3>
<p>Una vez instalado, vamos a crear un directorio donde almacenar todos nuetros
entornos, por ejemplo:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ mkdir virtualenvs
</pre></div>
<p>Ahora creamos un nuevo entorno dentro del directorio, como no queremos que use
ninguna libreria del sistema añadimos el parametro --no-site-packages</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ virtualenv --no-site-packages virtualenvs/proyecto1_env
</pre></div>
</div>
<div class="section" id="instalar-django-en-el-entorno">
<h3>Instalar django en el entorno</h3>
<p>Antes de instalar un nuevo paquete, tenemos que activar el entorno, para que
cambie los directorios de busquesda a los del entorno:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ <span class="nb">source</span> virtualenvs/proyecto1_env/bin/activate
</pre></div>
<p>Ahora ya podemos instalar django y los paquetes que necesitemos para nuestra
aplicación:</p>
<div class="highlight"><pre><span></span><span class="o">(</span>proyecto1_env<span class="o">)</span>secnot@secnot:~$ pip install django
<span class="o">(</span>proyecto1_env<span class="o">)</span>secnot@secnot:~$ pip install django-countries
<span class="o">(</span>proyecto1_env<span class="o">)</span>secnot@secnot:~$ django-admin.py startproject proyecto1
</pre></div>
<p>Para instalar un paquete no es necesario ser root, todos los paquetes se
almacenan en los directorios creados dentro de nuestro directorio.
Si te fijas verás que el prompt ha cambiado, indicando el entorno que está
activo.</p>
<p>Una vez no necesitemos el entorno podemos salir ejecutando:</p>
<div class="highlight"><pre><span></span><span class="o">(</span>proyecto1_env<span class="o">)</span>secnot@secnot:~$ deactivate
</pre></div>
</div>
<div class="section" id="requirements">
<h3>Requirements</h3>
<p>Una funcionalidad muy útil es la generación de una lista con todos los paquetes
instalados en un entorno, esta normalmente se almacenan en un archivo llamado
<em>requirements.txt</em>. Este archivo puede usarse después para duplicar los
paquetes instalados al crear un nuevo entorno. Para generarla:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ <span class="nb">source</span> environments/env1/bin/activate
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ pip freeze
<span class="nv">Django</span><span class="o">==</span><span class="m">1</span>.6
<span class="nv">gunicorn</span><span class="o">==</span><span class="m">18</span>.0
<span class="nv">wsgiref</span><span class="o">==</span><span class="m">0</span>.1.2
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ pip freeze > requirements.txt
</pre></div>
<p>Si queremos instalar todos los paquetes de la lista en un entorno:</p>
<div class="highlight"><pre><span></span><span class="o">(</span>env3<span class="o">)</span>secnot@secnot:~$ pip install -r requirements.txt
</pre></div>
</div>
</div>
<div class="section" id="virtualenvwrapper">
<h2>Virtualenvwrapper</h2>
<p>Virtualenvwrapper es un conjunto de scripts que automatizan la creación,
borrado, y gestión de entornos. Se puede trabajar directamente con virtualenv
sin ningún problema, pero virtualenvwrapper hace que todo el proceso sea un
poco más sencillo y cómodo.</p>
<div class="section" id="instalacion-y-configuracion">
<h3>Instalación y configuración</h3>
<p>Instalamos virtualenvwrapper con el gestor de paquetes:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ sudo apt-get install virtualenvwrapper
</pre></div>
<p>Por defecto los entornos se almacenan en el directorio <strong>.virtualenvs</strong> y no
necesita mas configuración simplemente sal y vuelve ha hacer login en la cuenta
para que se configuren las variables de entorno.</p>
<p>Si por qualquier razón en tu sistema no ha funcionado, puedes configurarlo manualmente
y añadirlo en el script de inicio <strong>.bashrc</strong>:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ mkdir .virtualenvs
</pre></div>
<div class="highlight"><pre><span></span><span class="c1"># /home/secnot/.bashrc</span>
<span class="c1"># Configuración de VIRTUALENVWRAPPER</span>
<span class="nb">export</span> <span class="nv">WORKON_HOME</span><span class="o">=</span><span class="nv">$HOME</span>/.virtualenvs
<span class="nb">source</span> /usr/local/bin/virtualenvwrapper.sh
<span class="c1"># Ubuntu 14.04: source /etc/bash_completion.d/virtualenvwrapper</span>
</pre></div>
</div>
<div class="section" id="uso">
<h3>Uso</h3>
<p>Estos son los comandos de virtualenvwrapper más usados, para mas detalles es
recomendable leer la documentación:</p>
<ul class="simple">
<li><strong>mkvirtualenv</strong>: Crea un nuevo entorno, con la particularidad de que se convierte en el entorno activo.</li>
<li><strong>cpvirtualenv</strong>: Duplica un entorno.</li>
<li><strong>rmvirtualenv</strong>: Elimina un entorno, el entorno a elminar debes estar inactivo antes de eliminarlo.</li>
<li><strong>allvirtualenv</strong>: Ejecuta un comando en todos los entornos.</li>
<li><strong>workon</strong>: Selecciona el entorno activo, si no hay argumentos lista los entornos disponibles.</li>
<li><strong>deactivate</strong>: Desactiva el entorno indicado.</li>
</ul>
<p>El mejor método para entender como funciona es un ejemplo, así el vamos a crear
dos entornos, despues instalar django en el primero de ellos, y por último
eliminar el segundo entorno:</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ mkvirtualenv --no-site-packages env1
New python executable in env1/bin/python
Installing setuotools, pip...done.
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ mkvirtualenv --no-site-packages env2
New python executable in env2/bin/python
Installing setuotools, pip...done.
<span class="o">(</span>env2<span class="o">)</span>secnot@secnot:~$ workon env1
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ pip install django
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ workon
env1
env2
<span class="o">(</span>env1<span class="o">)</span>secnot@secnot:~$ deactivate
secnot@secnot:~$ rmvirtualenv env2
Removing env2...
secnot@secnot:~$ ...
</pre></div>
<p>Si quieres usar una versión de Python distinta a la por defecto del sistema, puedes especificarla
en el momento de creación del entorno.</p>
<div class="highlight"><pre><span></span>secnot@secnot:~$ mkvirtualenv -p /usr/bin/python3 --no-site-packages env1
</pre></div>
</div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://simononsoftware.com/virtualenv-tutorial/">Virtualenv Tutorial</a></li>
<li><a class="reference external" href="http://www.virtualenv.org/">Virtualenv Documentation</a></li>
<li><a class="reference external" href="http://www.jeffknupp.com/blog/2013/12/18/starting-a-django-16-project-the-right-way/">Starting Django Project the right way</a></li>
<li><a class="reference external" href="http://virtualenvwrapper.readthedocs.org/en/latest/">Virtualevwrapper Documentation</a></li>
</ul>
</div>
Protección frente a ataques de red en linux2014-01-31T10:32:00+01:002014-01-31T10:32:00+01:00SecNottag:secnot.com,2014-01-31:/proteccion-ataques-red.html<p class="first last">Protege tu servidor de los ataques más comunes usando iptables.</p>
<p>En el artículo <a class="reference external" href="https://secnot.com/ataques-tcp.html">Ataquest TCP Comunes</a> expliqué
algunos de los ataques más comunes, en este post explico como configurar
linux para protegerse de esos ataques, y alguno más.</p>
<div class="section" id="ataque-syn-flood">
<h2>Ataque SYN Flood</h2>
<div class="section" id="syn-cookies">
<h3>SYN Cookies</h3>
<p>Linux implementa un metodo llamado
<a class="reference external" href="http://es.wikipedia.org/wiki/SYN_cookies">SYN Cookies</a>
para la protección contra este ataque, la técnica se consiste en elegir el
numero de secuencia del paquete <strong>SYN+ACK</strong> de manera que el servidor no
necesite guardar el estado de la conexión, y pueda usas el numero de secuencia
de la respuesta <strong>ACK</strong> del cliente, para reconstruir la entrada en la tabla de
conexiones.</p>
<p>Desde la versión del kernel 2.6.33, donde el sistema se modificó para
soportar <em>window scaling</em>, esta opción está activada por defecto. Puedes
comprobarlo usando:</p>
<div class="highlight"><pre><span></span>cat /proc/sys/net/ipv4/tcp_syncookies
</pre></div>
<div class="highlight"><pre><span></span>sysctl -n net.ipv4.tcp_syncookies
</pre></div>
<p>Si no está activado, puedes editar el archivo de configuración
<strong>/etc/sysctl.conf</strong> y añadir:</p>
<div class="highlight"><pre><span></span>net.ipv4.tcp_syncookies <span class="o">=</span> <span class="m">1</span>
</pre></div>
<p>Despues de salvar el archivo, puedes activarlo usando:</p>
<div class="highlight"><pre><span></span>sudo sysctl -p
</pre></div>
<p>Un servidor con tcp_syncookies activado se comporta de forma normal, hasta que
la cola de conexiones pendientes está llena, a partir de ese momento comienza
a enviar SYN cookies hasta que la congestión se alivie.</p>
<p>Si el servidor esta soportando mucha carga, puede ocurrir que tcp_syncookies
se active y cierre conexiones legítimas. En esos casos es importante tener en
cuenta que aunque puede ser un alivio temporal, SYN Cookies está diseñado para
proteger el sistema de ataques SYN Flood, y que usarlo para solucionar
problemas de congestión reiterados es contraproducente.</p>
</div>
<div class="section" id="iptables">
<h3>Iptables</h3>
<p>La segunda opción es usar iptables para limitar el número de paquetes SYN,
que son aceptados para cada dirección IP en un intervalo de tiempo. Para ello
usamos el patch <a class="reference external" href="http://www.netfilter.org/documentation/HOWTO/netfilter-extensions-HOWTO-3.html#ss3.16">recent</a> de iptables.</p>
<div class="highlight"><pre><span></span>iptables -A INPUT -p tcp -m state --state NEW -m recent --set --name sattack
iptables -A INPUT -p tcp -m state --state NEW -m recent --rcheck --name sattack --seconds <span class="m">60</span> --hitcount <span class="m">20</span> -j DROP
</pre></div>
<p>La primera regla añade la dirección de origen de los paquetes <strong>SYN</strong> a una
tabla llamada sattack, la segunda regla comprueba si en los últimos 60
segundos ha habido más de 20 paquetes <strong>SYN</strong>, desde la dirección de origen
del paquete, de ser así el paquete es descartado.</p>
<p>En caso de que se esté usando IP Spoofing junto a SYN Flood, la única opción
restante es intenta detectar alguna particularidad de la cabecera,
que permita diferenciar los paquetes del atacante del resto,
por ejemplo si MSS no tiene un valor correcto:</p>
<div class="highlight"><pre><span></span>iptables -t mangle -I PREROUTING -p tcp -m tcp --dport <span class="m">80</span> -m state --state NEW -m tcpmss ! --mss <span class="m">536</span>:65535 -j DROP
</pre></div>
<p>Estas reglas pueden interferir con proxies, y con clientes detrás de un NAT,
así que no es recomendable usarlas en un sistema que no esté bajo ataque.</p>
</div>
</div>
<div class="section" id="tcp-scans">
<h2>TCP Scans</h2>
<p>El sistema más sencillo y general para detectar/detener un scan, es usar
puertos cerrados como centinelas, de manera que si detectamos un paquete
que llega a alguno de esos puertos, podamos asumir que el paquete es parte
de un scan, y bloquear el acceso de la ip de origen temporalmente.</p>
<p>Es probable que el scan se centre únicamente en los puertos de servicios
más comunes, por lo que es recomendable usar como centinelas puertos
reservados por algún servicio, pero que no estén abiertos en ese servidor,
por ejemplo SMTP(25), DNS(53), NETBIOS(139). Ademas de esos puertos deberemos
añadir cuantos sean necesarios, para intentar detectar un scan, antes de que
el atacante consiga escanear un puerto abierto.</p>
<p>Por ejemplo si se ha cambiado el puerto por defecto de ssh es muy recomendable
añadir el puerto 22. Ten cuidado de NO usar como centinela un puerto abierto.</p>
<div class="highlight"><pre><span></span><span class="c1"># Añadimos los puertos ftp, telnet, smtp, netbios-ssn</span>
iptables -A INPUT -p tcp --dport <span class="m">21</span> -m recent --name portscan --set
iptables -A INPUT -p tcp --dport <span class="m">23</span> -m recent --name portscan --set
iptables -A INPUT -p tcp --dport <span class="m">25</span> -m recent --name portscan --set
iptables -A INPUT -p tcp --dport <span class="m">139</span> -m recent --name portscan --set
<span class="c1"># Usar como centinelas el puerto ssh y su substituto más común</span>
iptables -A INPUT -p tcp --dport <span class="m">22</span> -m recent --name portscan --set
iptables -A INPUT -p tcp --dport <span class="m">2222</span> -m recent --name portscan --set
<span class="c1"># Bloquear IP por 12 horas si el paquete esta en la tabla portscan</span>
iptables -A INPUT -m recent --name portscan --rchek --seconds <span class="m">43200</span> -j DROP
</pre></div>
<p>Algunos tipos de scan, usan paquetes construidos de manera que son fáciles de
diferenciar del tráfico legítimo, en ese caso es posible descartarlos
directamente:</p>
<div class="highlight"><pre><span></span><span class="c1"># XMAS Scan</span>
iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
<span class="c1"># NULL Scan</span>
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
<span class="c1"># SYN/RST Scan</span>
iptables -A INPUT -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
<span class="c1"># SYN/FIN Scan</span>
iptables -A INPUT -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
</pre></div>
</div>
<div class="section" id="ip-spoofing">
<h2>IP Spoofing</h2>
<p>Ip spoofing consiste en falsificar la dirección de origen de los paquetes,
en conjunción con algún ataque (pej SYN Flood), hace más dificil detectar
su origen, complica el filtrado de los paquetes, y según la configuración
pueden permitir saltarse algún firewall.</p>
<p>La única posibilidad es eliminar los paquetes que lleguen a un interface de
red, con una dirección de origen que no esten en el rango de direcciones
validas para ese interface:</p>
<div class="highlight"><pre><span></span>iptables -A INPUT -i eth0 -s <span class="m">0</span>.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s <span class="m">127</span>.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s <span class="m">10</span>.0.0.0/8 -j DROP
iptables -A INPUT -i eth0 -s <span class="m">172</span>.16.0.0/12 -j DROP
iptables -A INPUT -i eth0 -s <span class="m">192</span>.168.0.0/16 -j DROP
iptables -A INPUT -i eth0 -s <span class="m">224</span>.0.0.0/3 -j DROP
</pre></div>
<p>Tambien se puede activar la verificación de direcciones de origen del kernel,
que proporciona protección anti spoofing. Para comprobar si está activo:</p>
<div class="highlight"><pre><span></span>sysctl -n net.ipv4.conf.all.rp_filter
</pre></div>
<p>Si no lo esta edita <strong>/etc/sysctl.conf</strong> y añade:</p>
<div class="highlight"><pre><span></span><span class="c1"># Proteccion contra IP Spoofing</span>
net.ipv4.conf.all.rp_filter <span class="o">=</span> <span class="m">1</span>
</pre></div>
</div>
<div class="section" id="ataque-smurf">
<h2>Ataque Smurf</h2>
<p>Consiste en enviar paquetes de broadcast <strong>ICMP</strong> con la dirección de origen
falseada, de manera que la respuesta al paquete por parte de todos los equipos
en la red, genere una denegación de servicio en el equipo cuya direccion
fue suplantada en los paquetes.</p>
<p>La solución es no responde a los paquetes <strong>ICMP</strong> de broadcast.
El kernel se puede configurar para que ignore estos paquetes, así que iptables
no son necesarias. Si por defecto no esta activado, añade a <strong>/etc/sysctl.cof</strong>:</p>
<div class="highlight"><pre><span></span><span class="c1"># Ignorar peticiones echo broadcast para evitar ataque smurf</span>
net.ipv4.icmp_echo_ignore_broadcasts <span class="o">=</span> <span class="m">1</span>
</pre></div>
<p>Hoy en día es un ataque poco usado.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://www.ramil.pro/2013/07/linux-syn-attacks.html">SYN Flood Protection</a></li>
<li><a class="reference external" href="https://wiki.ubuntu.com/ImprovedNetworking/KernelSecuritySettings">Kernel Security Settings</a></li>
<li><a class="reference external" href="http://en.wikipedia.org/wiki/Smurf_attack">Smurf attack</a></li>
<li><a class="reference external" href="http://es.wikipedia.org/wiki/SYN_cookies">SYN Cookies</a></li>
</ul>
</div>
Ataques TCP comunes2014-01-29T15:04:00+01:002014-01-29T15:04:00+01:00SecNottag:secnot.com,2014-01-29:/ataques-tcp.html<p class="first last">Guía sobre los ataques tcp más comunes en internet.</p>
<p><strong>Aviso:</strong> <em>Este artículo asume conocimiento del protocolo TCP</em></p>
<p>Este es un listado de los ataques TCP más comunes que te puedes encontrar en
un servidor en internet.</p>
<div class="section" id="syn-flood">
<h2>SYN Flood</h2>
<p>Cuando un cliente inicia una conexión TCP con el servidor, lo hace en un
intercambio de 3 paquetes llamado 3-way handshake:</p>
<blockquote>
<ul class="simple">
<li>1 El cliente inicia la petición de conexión enviando un paquete SYN
al servidor</li>
<li>2 El servidor responde enviando un paquete SYN-ACK</li>
<li>3 El cliente responde con un paquete ACK para estableces la conexión</li>
</ul>
</blockquote>
<p>El ataque consiste en iniciar la petición de conexión enviando un paquete
<strong>SYN</strong> tras otro, al llegar al servidor cada petición abre una nueva entrada
en la tabla de conexiones, y envía un paquete <strong>SYN/ACK</strong> que el atacante
ignora.
El servidor espera un tiempo antes de descartar un conexión que no fue exitosa,
y con la llegada de cada vez más peticiones, la tabla de conexiones se va
llenando hasta que no quedan recursos para gestionar el trafico legitimo.</p>
<p>Las ventajas desde el punto de vista del atacante, es que el ancho de banda
necesario para este ataque es relativamente bajo, especialmente si se usa
<strong>ip-spoofing</strong> para falsificar la dirección IP de origen de los paquetes SYN,
de manera que la respuesta SYN/ACK se envían a un tercero.</p>
<p><a class="reference external" href="http://en.wikipedia.org/wiki/SYN_flood">SYN Flood (Wikipedia)</a></p>
</div>
<div class="section" id="syn-scan">
<h2>SYN Scan</h2>
<p>Un escaneo no es un ataque per se, pero suele ser un precursor a un ataque en
el que se escanean los puertos, para buscar servicios disponibles.</p>
<p>En un escaneo SYN en lugar de conectar al puerto para determinar si está
abierto, un atacante envía un paquete <strong>SYN</strong> y espera a la respuesta, si es
<strong>SYN/ACK</strong> indica que el puerto esta abierto, si no hay respuesta o es un
paquete RST el puerto está cerrado.
Si la respuesta es un paquete <strong>SYN/ACK</strong>, el atacante no envía el paquete
<strong>ACK</strong> de respuesta, no finalizando la conexión lo que hace que sea más
difícil de detectar.</p>
</div>
<div class="section" id="xmas-fin-null-scan">
<h2>XMAS/FIN/NULL Scan</h2>
<p>Un escaneo XMAS envía un paquete tcp con los <em>flags</em> <strong>FIN</strong>, <strong>URG</strong>, y
<strong>PSH</strong> activados.
Si el puerto esta abierto no hay respuesta, pero si el puerto está cerrado
el destinatario responde con un paquete <strong>RST/ACK</strong>. Este método sólo funciona
en sistemas que siguen las especificaciones RFC793.</p>
<p>Un escaneo FIN es similar a un scan XMAS pero envía un paquete con el <em>flag</em>
<strong>FIN</strong> activo, recibe las mismas respuestas que XMAS</p>
<p>NULL es similar a los otros dos pero envía un paquete con ningún <em>flag</em> activo.</p>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://en.wikipedia.org/wiki/Transmission_Control_Protocol">TCP (Wikipedia)</a></li>
</ul>
</div>
Firewall mínimo para servidor web2014-01-28T20:04:00+01:002014-01-28T20:04:00+01:00SecNottag:secnot.com,2014-01-28:/firewall-web-minimo.html<p class="first last">Script para configurar el firewall de un servidor web.</p>
<p>Acabo de configurar un
<a class="reference external" href="http://es.wikipedia.org/wiki/Servidor_virtual_privado">VPS</a>
como servidor web, y estas son las reglas iptables con las que empiezo todo
firewall, a partir de aquí voy añadiendo todos los servicios adicionales
que se necesiten.</p>
<pre class="code shell literal-block">
<span class="ch">#!/bin/bash
</span>
<span class="c1"># Limpiar todas las reglas.
</span>iptables -F
iptables -t nat -F
iptables -t mangle -F
<span class="c1"># Creamos tabla para reglas de seguridad eth0
</span>iptables -N security_eth0
iptables -F security_eth0
iptables -A INPUT -i eth0 -j security_eth0
<span class="c1"># Admitimos todos los paquetes en interface loopback.
</span>iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
<span class="c1"># Aceptamos todos los paquetes de conexiones ya establecidas
</span>iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
<span class="c1"># Permitir todas conexiones HTTP entrantes
</span>iptables -A INPUT -i eth0 -p tcp --dport <span class="m">80</span> -m state --state NEW -j ACCEPT
<span class="c1"># Permitir todas conexiones SSH entrantes
</span>iptables -A INPUT -i eth0 -p tcp --dport <span class="m">22</span> -m state --state NEW -j ACCEPT
<span class="c1"># Permitir conexiones SSH salientes (DESACTIVADO)
# iptables -A OUTPUT -o eth0 -p tcp --dport 22 -m state --state NEW -j ACCEPT
</span>
<span class="c1"># Permitir icmp (ping,....)
</span>iptables -A INPUT -p icmp -j ACCEPT
iptables -A OUTPUT -p icmp -j ACCEPT
<span class="c1"># Permitir acceso DNS saliente
</span>iptables -A OUTPUT -p udp -o eth0 --dport <span class="m">53</span> --sport <span class="m">1024</span>:65535 -j ACCEPT
iptables -A INPUT -p udp -i eth0 --sport <span class="m">53</span> --dport <span class="m">1024</span>:65535 -j ACCEPT
<span class="c1"># Por defecto descartamos los paquetes no aceptados explicitamente
</span>iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
<span class="c1">#
# Medidas de seguridad para paquetes entrantes eth0
#
</span>
<span class="c1"># echo 1 > /proc/sys/net/ipv4/tcp_syncookies
</span>
<span class="c1"># Descomentar la siguiente linea para desactivar la tabla security_eth0
# iptables -A security_eth0 -j RETURN
</span>
<span class="c1"># Ignoramos paquetes nuevos que no tengan flag SYN activado
</span>iptables -A security_eth0 -p tcp ! --syn -m state --state NEW -j DROP
<span class="c1"># Ignoramos paquetes invalidos
</span>iptables -A security_eth0 -m state --state INVALID -j DROP
<span class="c1"># Descartar paqueter fragmentados
</span>iptables -A security_eth0 -f -j DROP
<span class="c1"># Descartar paquetes XMAS
</span>iptables -A security_eth0 -p tcp --tcp-flags ALL ALL -j DROP
<span class="c1"># Descartart paquetes NULL
</span>iptables -A security_eth0 -p tcp --tcp-flags ALL NONE -j DROP
<span class="c1"># Descartar ip falsas (spoofing)
</span>iptables -A security_eth0 -s <span class="m">0</span>.0.0.0/8 -j DROP
iptables -A security_eth0 -s <span class="m">127</span>.0.0.0/8 -j DROP
iptables -A security_eth0 -s <span class="m">10</span>.0.0.0/8 -j DROP
iptables -A security_eth0 -s <span class="m">172</span>.16.0.0/12 -j DROP
iptables -A security_eth0 -s <span class="m">192</span>.168.0.0/16 -j DROP
iptables -A security_eth0 -s <span class="m">224</span>.0.0.0/3 -j DROP
</pre>
<p><a class="reference external" href="https://secnot.com/scripts/firewall-web-minimo.sh">Descargar</a></p>
<p>Este script bloquea todo tráfico entrante y saliente, excepto conexiones
entrantes a los puertos html y ssh, el protocolo icmp, y las consultas DNS
salientes.
También crea la tabla <em>security_eth0</em>, donde se filtran todos los paquetes
entrantes por eth0, para realizar algunos chequeos de seguridad.</p>
<p>Si no quieres añadir el script directamente a init.d, puedes introducir las
reglas manualmente en el shell, y luego usar <em>iptables-persistent</em> para
salvarlas.</p>
<div class="highlight"><pre><span></span>$ sudo apt-get install iptables-persistent
</pre></div>
<p>Durante la instalación preguntará si quieres salvar las reglas de iptables,
responde si, y serán salvadas en <strong>/etc/iptables/rules.v4</strong> para ipv4, y
<strong>/etc/iptables/rules.v6</strong> para ipv6. Por último iniciamos el servicio:</p>
<div class="highlight"><pre><span></span>$ sudo service iptables-persistent start
</pre></div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<p><a class="reference external" href="http://www.linuxtopia.org/Linux_Firewall_iptables/x6193.html">Iptables state NEW and --syn</a></p>
</div>
Introducción a la Inyección de SQL2014-01-27T15:04:00+01:002015-06-24T22:10:00+02:00SecNottag:secnot.com,2014-01-27:/video-sql-injection.html<p class="first last">Video sobre inyección de SQL (Ingles)</p>
<p>Hace unos días encontré un video interesante sobre
<a class="reference external" href="http://es.wikipedia.org/wiki/Inyección_SQL">inyección SQL</a> en PHP
(Inglés), y lo que me pareció más curioso, es que aún siga siendo un
problema de seguridad tan extendido, he buscado las cifras y es el segundo
ataque más usado con un 16%, detras de
<a class="reference external" href="http://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting</a> con
un 37%.</p>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="youtube"><iframe src="https://www.youtube.com/embed/_jKylhJtPmI" width="640" height="360" allowfullscreen seamless frameBorder="0"></iframe></div><div class="line-block">
<div class="line"><br /></div>
</div>
<p>Si te ha gustado el video, el canal
<a class="reference external" href="http://www.youtube.com/user/Computerphile?feature=watch">Computerphile</a>
tiene muchos más videos interesantes relacionados con la informática.</p>
Detección de intrusos con AIDE2014-01-22T13:12:00+01:002014-01-22T13:12:00+01:00SecNottag:secnot.com,2014-01-22:/deteccion-intrusos-aide.html<p class="first last">Tutorial sobre el uso de AIDE en Ubuntu/Debian</p>
<p>AIDE (Advanced Intrusion Detection Environment) es un programa de detección
de intrusos, en especifico un monitor de integridad de archivos. Cuando un
intruso consigue acceso a un sistema, una sus primera acciones será modificar
los logs del sistema, y/o modificar algún programa para mantener el control,
y ocultar su presencia. Por ejemplo puede sustituir el programa ps para que
no liste determinados procesos, añadir el bit SUID a una copia del shell,
o instalar algún rootkit.</p>
<p>Detectar esto es prácticamente imposible manualmente, AIDE en cambio nos
permite hacerlo de forma rápida y sencilla, haciendo imposible cualquier
manipulación de los archivo o directorios monitorizados.</p>
<div class="section" id="instalacion">
<h2>Instalación</h2>
<p>La instalación es sencilla la única dependencia del paquete, es un servidor
de correo para poder enviar los informes y alertas al root, o a la dirección
especificada, asegurate de que esta funcionando correctamente, antes de
continuar con la configuración.</p>
<div class="highlight"><pre><span></span>sudo apt-get install aide
</pre></div>
</div>
<div class="section" id="configuracion">
<h2>Configuración</h2>
<p>Antes de modificar la configuración por defecto de aide, vamos a comprobar que
funciona correctamente, para ello generamos la primera base de datos. Este
proceso puede ser muy lento dependiendo del ordenador, y del numero de archivos
a monitorizar.</p>
<div class="highlight"><pre><span></span>sudo aideinit
</pre></div>
<p>Una vez generada, la copiamos para instalarla:</p>
<div class="highlight"><pre><span></span>sudo cp /var/lib/aide.db.new /var/lib/aide.db
</pre></div>
<p>Ahora podemos modificar la configuración para incluir cualquier directorio
que necesitemos monitorizar, o eliminar algún directorio en el que no estemos
interesados. Los archivos de configuración de AIDE en Debian/Ubuntu,
son <strong>/etc/aide/aide.conf</strong> y el directorio <strong>/etc/aide/aide.conf.d/</strong>
, estos no son usados directamente por AIDE, sino que el script
<strong>update-aide.config</strong> recopila toda la información y, crea el archivo
final <strong>/var/lib/aide/config.autogenerated</strong>.</p>
<p>Para añadir nuestros directorios a la configuración, tenemos que crear un
archivo en el directorio <strong>/etc/aide/aide.conf.d</strong></p>
<div class="highlight"><pre><span></span>sudo touch /etc/aide/aide.conf.d/60_aide_custom
sudo chmod <span class="m">644</span> /etc/aide/aide.conf.d/60_aide_custom
</pre></div>
<p>El número al inicio indica el orden en el que el archivo es cargado,
hay que asegurarse de que el numero de nuestro archivo es el mayor del
directorio, de esta manera nuestra configuración sea la última en cargar y no
tengamos conflictos.</p>
<p>Cada archivo contiene la lista de directorios y archivos que tienen que ser
monitorizados por AIDE, por ejemplo los archivos de configuración de una
aplicación, el directorio de contenidos estáticos de una pagina web, o el
directorio donde se guardan los backups, etc. Tras cada una de esas rutas,
se añade la lista de aspectos que tiene que ser monitorizados, usando el
mismo formato definido en el <a class="reference external" href="http://aide.sourceforge.net/stable/manual.html">manual de AIDE</a></p>
<p>En el siguiente ejemplo se pueden ver algunas de las posibilidades que ofrece
AIDE, otra buena fuente son los archivos de configuración en
<strong>/etc/aide/aide.conf.d/</strong></p>
<div class="highlight"><pre><span></span><span class="c1"># Parametros que pueden ser comprobados</span>
<span class="c1">#</span>
<span class="c1"># p: permissions</span>
<span class="c1"># ftype: file type</span>
<span class="c1"># i: inode</span>
<span class="c1"># n: number of links</span>
<span class="c1"># l: link name</span>
<span class="c1"># u: user</span>
<span class="c1"># g: group</span>
<span class="c1"># s: size</span>
<span class="c1"># b: block count</span>
<span class="c1"># m: mtime</span>
<span class="c1"># a: atime</span>
<span class="c1"># c: ctime</span>
<span class="c1"># S: check for growing size</span>
<span class="c1"># I: ignore changed filename</span>
<span class="c1"># md5: md5 checksum</span>
<span class="c1"># sha1: sha1 checksum</span>
<span class="c1"># sha256: sha256 checksum</span>
<span class="c1"># sha512: sha512 checksum</span>
<span class="c1"># rmd160: rmd160 checksum</span>
<span class="c1"># tiger: tiger checksum</span>
<span class="c1"># haval: haval checksum</span>
<span class="c1"># crc32: crc32 checksum</span>
<span class="c1"># R: p+ftupe+i+l+n+u+g+s+m+c+md5</span>
<span class="c1"># L: p+ftype+i+l+n+u+g</span>
<span class="c1"># E: Empty group</span>
<span class="c1"># >: Growing logfile p+ftype+l+u+g+i+n+S</span>
<span class="c1"># The following are available if you have mhash support enabled:</span>
<span class="c1"># gost: gost checksum</span>
<span class="c1"># whirlpool: whirlpool checksum</span>
<span class="c1"># The following are available and added to the default groups R, L and ></span>
<span class="c1"># only when explicitly enabled using configure:</span>
<span class="c1"># acl: access control list</span>
<span class="c1"># selinux SELinux security context</span>
<span class="c1"># xattrs: extended file attributes</span>
<span class="c1"># e2fsattrs: file attributes on a second extended file system</span>
<span class="c1"># Creamos nuestras reglas.</span>
<span class="nv">MinimalCheck</span> <span class="o">=</span> p+i+u+g
<span class="nv">FullCheck</span> <span class="o">=</span> p+i+n+u+g+s+b+c+sha256
<span class="c1"># Directorios a monitorizar</span>
/home/secnot/private/website MinimalCheck
/home/secnot/chroot FullCheck
<span class="c1"># Archivo a monitorizar</span>
<span class="c1"># a diferencia de los directorios los archivos son terminados en '$'</span>
<span class="c1"># antes de '.' hace falta un caracter de escape '\'</span>
/home/secnot/mi_aplicacion<span class="se">\.</span>conf$ FullCheck
<span class="c1"># Usar las reglas no es necesario pero es más legible</span>
/etc/mi_otra_aplicacion<span class="se">\.</span>conf$ p+i+g+d
<span class="c1"># Podemos usar las reglas definidas en /etc/aide/aide.conf,</span>
<span class="c1"># como en los otros archivos de configuración</span>
/home/webuser/webapp VarLog
<span class="c1"># Forzamos que se ignore un archivo o varios</span>
!/etc/app<span class="se">\.</span>conf$
!/var/log/.*
</pre></div>
<p>Por último solo nos queda editar <strong>/etc/default/aide</strong>, en el cual podemos
definir donde deseamos que lleguen los informes generados periódicamente
(por defecto root), junto algunas opciones más sobre los mismos.</p>
</div>
<div class="section" id="uso">
<h2>Uso</h2>
<p>Cada vez que realicemos alguna modificación, instalemos, o eliminemos algún
programa, generamos una nueva base de datos, a partir de ese momento cada
vez que se ejecute AIDE se comprobará que nada ha sido alterado con respecto
al estado almacenado en la base de datos.</p>
<div class="highlight"><pre><span></span>sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
</pre></div>
<p>Como medida de seguridad adicional, es recomendable calcular un hash de la base
de datos, para poder comprobar manualmente que no ha sido modificada por un
atacante. Guarda una copia del hash en un lugar seguro.</p>
<div class="highlight"><pre><span></span>sha256sum /var/lib/aide/aide.db
</pre></div>
<p>AIDE es ejecutado automáticamente todos los días por cron, a través de un
script en <strong>/etc/cron.daily/</strong>, este ejecuta update-aide.config para generar un
archivo de configuración actualizado, y después llama aide.
Una vez la comprobación ha terminado, almacena la información en los archivos
de log <strong>/var/log/aide/aide.log</strong> y de errores <strong>/var/log/aide/error.log</strong>,
por último envía un informe a la dirección de correo configurada si es que
una existe.</p>
<p>Si queremos comprobar si ha habido alguna modificación manualmente, primero
es recomendable comprobar que la base de datos no ha sido modificada usando
<em>sha256sum</em>, después podemos ejecutar el script:</p>
<div class="highlight"><pre><span></span>sudo /usr/bin/aide.wrapper
</pre></div>
<p>O podemos generar manualmente el archivo de configuración, y después
ejecutar aide.</p>
<div class="highlight"><pre><span></span>sudo update-aide.config
sudo aide -c /var/lib/aide/aide.conf.autogenerated --check
</pre></div>
</div>
<div class="section" id="enlaces">
<h2>Enlaces</h2>
<ul class="simple">
<li><a class="reference external" href="http://aide.sourceforge.net/stable/manual.html">AIDE Manual</a></li>
<li><a class="reference external" href="https://mailman.cs.tut.fi/pipermail/aide/2008-February/000903.html">Debian Aide Guide</a></li>
<li><a class="reference external" href="http://www.cyberciti.biz/faq/debian-ubuntu-linux-software-integrity-checking-with-aide/">nixCraft AIDE Guide</a></li>
<li><a class="reference external" href="https://help.ubuntu.com/community/FileIntegrityAIDE">Ubuntu Aide Help Page</a></li>
</ul>
</div>
Seguridad en SSH parte II2014-01-17T17:56:00+01:002014-01-17T17:56:00+01:00SecNottag:secnot.com,2014-01-17:/seguridad-ssh-2.html<p class="first last">Segunda parte del tutorial Seguridad en SSH</p>
<p>Continuamos <a class="reference external" href="https://secnot.com/seguridad-ssh-1.html">Seguridad SSH parte I</a> con más
trucos para hacer el servicio un poco más seguro. En esta segunda parte
tratamos de mejorar la seguridad de las claves, y dificultar la vida de un
aracante que consiga acceso.</p>
<div class="section" id="limitar-el-acceso-ssh-a-los-usuarios">
<h2>5. Limitar el acceso ssh a los usuarios</h2>
<p>Por defecto todos los usuarios del sistema pueden conectarse usando ssh, pero
si no todos los usuarios necesitan acceso ssh, por ejemplo tiene un usuario
sólo para acceder por ftp, es posible restringir su acceso en el archivo
<strong>/etc/ssh/sshd_config</strong></p>
<div class="highlight"><pre><span></span>DenyUsers ftpuser1, ftpuser2
</pre></div>
<p>o aún más seguro, restringir el acceso a todo los usuarios excepto los
especificados:</p>
<div class="highlight"><pre><span></span>AllowUsers secnot, juan
</pre></div>
</div>
<div class="section" id="desactivar-claves-vacias">
<h2>6. Desactivar claves vacías</h2>
<p>Comprobar que usuarios con claves vacías no puedan hacer login, normalmente
esta incluido en la configuración por defecto, pero es mejor comprobarlo
en <strong>/etc/ssh/sshd_config</strong>:</p>
<div class="highlight"><pre><span></span>PermitEmptyPasswords no
</pre></div>
</div>
<div class="section" id="firewall-para-el-puerto-ssh">
<h2>7. Firewall para el puerto SSH</h2>
<p>Si las conexiones a SSH van a ser siempre desde una ip fija, es posible
configurar el firewall para que permita conexiones sólo desde esa ip.
Esto no es una guía de iptables, pero para acceso exclusivo desde la ip
82.168.100.7, podría ser algo similar a esto:</p>
<div class="highlight"><pre><span></span>iptables -A INPUT -i eth0 -p tcp -s 82.168.100.7 --dport 22 -m state --state NEW,ESTABLISHED -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --dport 22 -j DROP
iptables -A OUTPUT -o eth0 -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
</pre></div>
<p>antes de modificar iptables asegúrate que sabes lo que haces, porque te puedes
quedar sin acceso remoto, si bloqueas el puerto por error.</p>
</div>
<div class="section" id="caducidad-de-sesiones-inactivas">
<h2>8. Caducidad de sesiones inactivas</h2>
<p>Cuando exista la posibilidad de que usuarios dejen abiertas sesiones ssh en
ordenadores inseguros, es recomendable configurar sshd, para que las sesiones
se cierren automáticamente tras un periodo de inactividad. En <strong>sshd_config</strong>
añadimos o modificamos:</p>
<div class="highlight"><pre><span></span><span class="c1"># Log Out tras 5 minutos</span>
ClientAliveInterval <span class="m">300</span>
ClientAliveCountMax <span class="m">0</span>
</pre></div>
<p>Esto establece un periodo de 5 minutos (300 segundos). Cuando la sesión de un
usuario está inactiva más de este intervalo, es cerrada automáticamente.</p>
</div>
<div class="section" id="fortaleza-de-las-claves">
<h2>9. Fortaleza de las claves</h2>
<p>Aunque no relacionado directamente con el servicio ssh, las claves de los
usuarios son uno de sus principales puntos débiles, un usuario con una clave
débil puede hacer todo el sistema vulnerable, aunque la mejor solución es
desactivar login con claves y usar exclusivamente autenticación pública,
desgraciadamente esto no es siempre posible, en estos casos se puede paliar
el problema obligando a los usuarios a elegir mejores claves.</p>
<p>Pluggable Authentication Modules (PAM) es el encargado de proporcionar
autenticación para usuarios y servicios en linux, y nos permite mediante
distintos módulos, configurar prácticamente todos los aspectos del proceso.
En este caso el modulo que nos interesa esta en <strong>/etc/pam.d/common-passwd</strong>
donde tendremos que editar la linea:</p>
<div class="highlight"><pre><span></span>#password [success=1 default=ignore] pam_unix.so obscure sha512
password [success=1 default=ignore] pam_unix.so obscure sha512 minlen=8
</pre></div>
<p>Así forzamos a que las claves sean al menos de 8 caracteres. Si esto no es
suficiente, puedes probar el modulo <strong>pam_cracklib</strong></p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo apt-get install libpam-cracklib
</pre></div>
<p>Tras instalar cracklib editamos <strong>/etc/pam.d/common-password</strong>:</p>
<div class="highlight"><pre><span></span>password requisite pam_cracklib.so <span class="nv">retry</span><span class="o">=</span><span class="m">3</span> <span class="nv">minlen</span><span class="o">=</span><span class="m">10</span> <span class="nv">difok</span><span class="o">=</span><span class="m">3</span> <span class="nv">ucredit</span><span class="o">=</span><span class="m">1</span> <span class="nv">lcredit</span><span class="o">=</span><span class="m">1</span> <span class="nv">dcredit</span><span class="o">=</span><span class="m">1</span>
</pre></div>
<p>Esto forzará que las claves sean como mínimo de 10 caracteres, que al menos
3 caracteres cambiaron respecto a la clave anterior, al menos un carácter esté
en minúsculas, uno en mayúsculas, y uno sea un número. Se pueden usar políticas
mucho más restrictivas usando cracklib, pero llevado al extremo comienza a ser
contraproducente, las claves resultantes son demasiado complejas para recordar, y acaban apuntadas en un post-it pegado al monitor.</p>
</div>
<div class="section" id="apparmor">
<h2>10. Apparmor</h2>
<p>En muchos casos los usuarios no necesitan tener más acceso que a su directorio
personal, y siempre es buena política dar a los usuarios exactamente los
mínimos privilegios posibles, tradicionalmente restringir el directorio se ha
hecho con chroot, pero hoy en día apparmor me parece una solución más limpia y
flexible.</p>
<p>Vamos a restringir el usuario juanjo, lo primero será crear una un link del
shell bash a jailbash, para poder modificar las opciones de apparmor de forma independiente.</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo ln /bin/bash /usr/local/bin/jailbash
</pre></div>
<p>Añadimos jailbash a la lista de shells disponibles en <strong>/etc/shells</strong></p>
<p>Y se asigna el nuevo shell al usuario a restringir</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo chsh -s /usr/local/bin/jaiblash juanjo
</pre></div>
<p>Tras esto creamos <strong>/etc/apparmor.d/usr.local.bin.jailbash</strong> que contiene el
perfil apparmor para el nuevo shell, incluyo un ejemplo como patrón del
contenido, pero es necesario adaptarlo a las necesidades de cada sistema
y las restricciones del usuario.</p>
<div class="highlight"><pre><span></span><span class="c1">#include <tunables/global></span>
/usr/local/bin/jailbash <span class="o">{</span>
<span class="c1">#include <abstractions/base></span>
<span class="c1">#include <abstractions/bash></span>
<span class="c1">#include <abstractions/consoles></span>
<span class="c1">#include <abstractions/nameservice></span>
<span class="c1">#include <abstractions/user-manpages></span>
<span class="c1">#include <abstractions/user-tmp></span>
deny /bin/df r,
deny /etc/bash_command_not_found r,
/bin/ r,
/bin/bash rix,
/bin/cat rix,
/bin/chmod rix,
/bin/chown rix,
/bin/cp rix,
/bin/date rix,
/bin/egrep rix,
/bin/grep rix,
/bin/gunzip rix,
/bin/gzip rix,
/usr/local/bin/jailbash rix,
/bin/ln rix,
/bin/ls rix,
/bin/mkdir rix,
/bin/mktemp rix,
/bin/more rix,
/bin/mv rix,
/bin/ping rix,
/bin/readlink rix,
/bin/rm rix,
/bin/rmdir rix,
/bin/sed rix,
/bin/sleep rix,
/bin/tar rix,
/bin/touch rix,
/bin/uname rix,
/bin/vim rix,
/bin/vim-normal rix,
/bin/zcat rix,
/dev/null rw,
/dev/urandom r,
/etc/ r,
/etc/manpath.config r,
/etc/opt/ r,
/etc/sysconfig/console r,
/etc/sysconfig/mail r,
/etc/sysconfig/news r,
/etc/vimrc r,
owner /home/*/ r,
owner /home/*/** rwl,
/opt/ r,
owner /proc/*/cmdline r,
owner /proc/*/exe r,
owner /proc/*/mounts r,
/tmp/** rw,
/proc/loadavg r,
/usr/bin/ r,
/usr/bin/groups rix,
/usr/bin/lesspipe rix,
/usr/bin/cut rix,
/usr/bin/whereis rix,
/usr/bin/sort rix,
/usr/bin/basename rix,
/usr/bin/head rix,
/usr/bin/id rix,
/usr/bin/less rix,
/usr/bin/man rix,
/usr/bin/manpath rix,
/usr/bin/mc rix,
/usr/bin/scp rix,
/usr/bin/screen rix,
/usr/bin/ssh rix,
/usr/bin/ssh-add rix,
/usr/bin/ssh-agent rix,
/usr/bin/ssh-copy-id rix,
/usr/bin/ssh-keygen rix,
/usr/bin/ssh-keyscan rix,
/usr/bin/tail rix,
/usr/bin/tty rix,
/usr/bin/vim rix,
/usr/bin/wget rix,
/usr/bin/which rix,
/usr/lib*/mc/cons.saver rix,
/usr/lib*/ssh/ssh-keysign rix,
/usr/local/bin/ r,
/usr/share/mc/** r,
/usr/share/vim/** r,
<span class="o">}</span>
</pre></div>
<p>Por último activamos apparmor para jailbash:</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo aa-enforce jailbash
Setting /etc/apparmor.d/usr.local.bin.jailbash to enforce mode.
</pre></div>
</div>
Seguridad en SSH parte I2014-01-14T10:20:00+01:002014-01-14T10:20:00+01:00SecNottag:secnot.com,2014-01-14:/seguridad-ssh-1.html<p class="first last">Primera parte del tutorial Seguridad en SSH</p>
<p>SSH (Secure Shell) es un protocolo de comunicación cifrado, que permite el
acceso remoto, y la ejecución remota de comandos. Probablemente sea el único
servicio disponible en todo servidor, y aunque el protocolo es seguro,
la configuración por defecto no suele ser la más adecuada.</p>
<p>Aparte se usar claves de calidad para los usuarios del servidor, hay muchas
pequeñas modificaciones que pueden incrementar sustancialmente la seguridad del
servicio.</p>
<div class="section" id="cambiar-el-puerto-por-defecto">
<h2>1. Cambiar el puerto por defecto</h2>
<p>Por defecto ssh escucha el puerto 22, de manera que es fácil para cualquier
atacante escanear el puerto para determinar si ssh se está ejecutando.
Cambiando el puerto se soluciona parcialmente el problema, los escaneos
automáticos no encontrarán el puerto, en cambio un atacante interesado
específicamente en tu servidor, puede escaneará todos los puertos hasta
encontrar ssh.</p>
<p>Aun así este cambio es muy sencillo, nos protege de ataques automáticos,
e hipotéticos exploits remotos que puedan aparecer algún día.</p>
<p>Para modificar el puerto sólo es necesario editar <strong>/etc/ssh/sshd_config</strong>
añadiendo o modificando la linea:</p>
<div class="highlight"><pre><span></span><span class="c1"># Nuevo puerto ssh</span>
Port <span class="m">7544</span>
</pre></div>
<p>y reiniciar el servicio sshd</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo service sshd restart
</pre></div>
<p>para conectar ahora hay que usar</p>
<div class="highlight"><pre><span></span>secnot@cliente:~$ ssh servidor -p 7544
</pre></div>
</div>
<div class="section" id="desactivar-login-al-root">
<h2>2. Desactivar login al root</h2>
<p>Acceder como root a cualquier sistema es una mala practica, especialmente en
logins remotos, siempre es preferible tener un usuario con permisos sudo
para ejecutar comandos como root. Si este es tu caso, es recomendable prohibir
el acceso por ssh al usuario root.</p>
<p>El primer paso es asegurarnos que tenemos al menos un usuario con permisos para
usar sudo, si no es así nos quedaríamos sin acceso como root al servidor.</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ whoami
secnot
secnot@servidor:~$ sudo whoami
[sudo] password for secnot:
root
</pre></div>
<p>Si da algun problema al ejecutar sudo no continuar con este paso. Para
modificar la configuracion editamos <strong>/etc/ssh/sshd_config</strong>:</p>
<div class="highlight"><pre><span></span><span class="c1"># Desactivar login para root</span>
PermitRootLogin no
</pre></div>
<p>y reiniciamos el servicio</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ sudo service sshd restart
</pre></div>
</div>
<div class="section" id="desactivar-protocolo-1">
<h2>3. Desactivar protocolo 1</h2>
<p>SSH puede usar dos protocolos 1 y 2, el protocolo 1 es el más antiguo y fue
remplazado por el 2, cuando se descubrieron ciertas vulnerabilidades, pero
algunas distribuciones de unix/linux, aún continúan permitiendo usar ambos
para mantener compatibilidad hacia atrás. Es recomendable desactivar el
protocolo 1, para ello editamos <strong>/etc/ssh/sshd_config</strong>:</p>
<div class="highlight"><pre><span></span><span class="c1"># Protocol 2,1</span>
Protocol <span class="m">2</span>
</pre></div>
<p>y reiniciamos el servicio</p>
</div>
<div class="section" id="usar-autenticacion-de-clave-publica">
<h2>4. Usar autenticación de clave Publica</h2>
<p>Tiene dos ventajas, la primera es que es posible hacer login sin introducir la
clave, y la segunda, es que podremos desactivar completamente el login con
claves, haciendo imposibles los ataques por fuerza bruta o diccionario.
El primer paso es generar un par del claves publica/privada en el cliente:</p>
<div class="highlight"><pre><span></span>secnot@cliente:~$ ssh-keygen -b 4096 -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/secnot/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/secnot/.ssh/id_rsa.
Your public key has been saved in /home/secnot/.ssh/id_rsa.pub.
The key fingerprint is:
0d:d2:94:07:93:a9:dd:b9:33:73:fb:f1:6e:3e:ce:d7 secnot@client
The keys randomart image is:
+--[ RSA 4096]----+
| .. |
| .. . . |
| .oo o. o |
| .+o.=. . . |
| ..oSo . |
| ooo. . |
| E.. . |
| . . .. |
| ..o. |
+-----------------+
</pre></div>
<p>Primero pregunta en que directorio deseas guardar el par de claves, a no ser
que tengas otro par y no quieras confusiones, usa el directorio por defecto.</p>
<p>En el segundo paso te pide un passphrase (una clave para proteger el par de
claves), si no se la proporcionas cualquiera que consiga acceso al par de
claves, tendrá acceso automático al servidor, a cambio no tendrás que
introducir una clave cuando hagas login. En cambio si proporcionas un
passphrase si que tendrás que introducir una clave para hacer login, pero si
el par de claves es robado, no podrán usarlo para acceder al servidor.
Tu decisión</p>
<p>Por último queda subir las claves al servidor, para ello hay que añadir el
contenido de la clave pública <strong>id_rsa.pub</strong> al archivo
<strong>~/.ssh/authorized_keys</strong> de tu usuario en el servidor. Para facilitar la
tarea existe la utilidad <em>ssh-copy-id</em>:</p>
<div class="highlight"><pre><span></span>secnot@cliente:~$ ssh-copy-id -i .ssh/id_rsa.pub secnot@servidor
secnot@servidor's password:
Now try logging into the machine, with "ssh 'secnot@servidor'", and check in:
~/.ssh/authorized_keys
to make sure we haven't added extra keys that you weren't expecting.
</pre></div>
<p>Si ssh-copy-id no está disponible en tu sistema, puedes hacerlo manualmente,
si no existe creamos el directorio .ssh en el servidor</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ mkdir .ssh
secnot@servidor:~$ chmod 700 .ssh
</pre></div>
<p>copiamos el archivo de clave publica <strong>.ssh/id_rsa.pub</strong> al servidor:</p>
<div class="highlight"><pre><span></span>secnot@cliente:~$ scp ~/.ssh/id_rsa.pub secnot@servidor:./
</pre></div>
<p>añadimos id_rsa.pub a <strong>~/.ssh/authorized_keys</strong>, y nos aseguramos que los
permisos del archivo sean los correctos</p>
<div class="highlight"><pre><span></span>secnot@servidor:~$ cat id_rsa.pub >> ~/.ssh/authorized_keys
secnot@servidor:~$ chmod 600 ~/.ssh/authorized_keys
secnot@servidor:~$ rm id_rsa.pub
</pre></div>
<p>Una vez tengas comprobado que todo funciona correctamente, y que puedes hacer
login usando ssh, ya sea sin clave o con tu passphrase, puedes dar el ultimo
paso, desactivar la autenticación por clave, modificando el archivo
<strong>/etc/ssh/sshd_config</strong></p>
<div class="highlight"><pre><span></span><span class="c1"># Desactivar logins usando claves</span>
PasswordAuthentication no
</pre></div>
<p>Por ultimo, asegúrate de guardar una copia de el par de claves <strong>id_rsa</strong> e
<strong>id_rsa.pub</strong>, si las pierdes no podrás acceder al servidor remotamente.</p>
<p><a class="reference external" href="https://secnot.com/seguridad-ssh-2.html">Seguridad SSH parte II</a></p>
</div>