nk.py 8.39 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

"""Utilitaires de communication avec le serveur NK2015"""

import json
import socket
import ssl
import urllib
import re
11
import time
12
import os.path
13 14 15

# Les objets de réponse HTTP
from django.http import HttpResponse, HttpResponseRedirect
16
from django.shortcuts import render
17 18 19 20 21 22 23 24 25 26 27

# Les paramètres django
import settings

# Les messages
import messages

#: Ce module contient les valeurs qu'on a besoin de conserver
#: d'une requête HTTP du client à une autre
import keep_alive

28
import basic
29

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59

class NKError(Exception):
    """Classe de base d'une erreur survenant pendant la communication
       avec le serveur NK2015.
       
       """
    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg
    def __str__(self):
        return str(self.msg)
    def __unicode__(self):
        return unicode(self.msg)

class NKRefused(NKError):
    """Levée en cas de connection refused."""
    pass

class NKHelloFailed(NKError):
    """Levée en cas d'échec au hello."""
    pass

class NKUnknownError(NKError):
    """Levée en cas d'autre erreur."""
    pass

class NKDeadServer(NKError):
    """Levée quand le serveur ne répond plus."""
    pass

60 61 62 63
class NKNotJson(NKError):
    """Levée quand le message transmis n'est pas un JSON."""
    pass

64
def full_read(socket):
Vincent Le gallic's avatar
Vincent Le gallic committed
65
    """Lit un message complet sur la socket."""
66
    # On récupère d'abord la taille du message
67 68 69 70 71 72
    length_str = ''
    char = socket.recv(1)
    while char != '\n':
        length_str += char
        char = socket.recv(1)
    total = int(length_str)
73
    # On utilise une memoryview pour recevoir les données chunk par chunk efficacement
74 75 76 77 78 79 80
    view = memoryview(bytearray(total))
    next_offset = 0
    while total - next_offset > 0:
        recv_size = socket.recv_into(view[next_offset:], total - next_offset)
        next_offset += recv_size
    try:
        msg = json.loads(view.tobytes())
81 82
    except (TypeError, ValueError) as e:
        raise NKNotJson("L'objet reçu n'est pas un JSON")
83
    return msg
84

85 86 87 88 89

def _is_success_code(cod):
    """Dit si un code de retour est un succès ou non"""
    return cod == 0 or 100 <= cod < 200

90
def connect_NK(request):
91
    """Connecte une socket au servuer NK2015 et la renvoie après avoir effectué le hello.
92
       ``ip_user`` est l'IP de l'utilisateur que django va transmettre au backend.
93
       Lève une erreur en cas d'échec"""
94 95
    # On récupère l'IP de l'utilisateur
    ip_user = basic.get_client_ip(request)
96 97 98 99 100
    sock = socket.socket()
    try:
        # On établit la connexion sur port 4242
        sock.connect((settings.NK2015_IP, settings.NK2015_PORT))
        # On passe en SSL
101
        ca_file = os.path.join(settings.PROJECT_PATH, "keys/ca.crt")
102
        sock = ssl.wrap_socket(sock, ca_certs=ca_file)
103

104
        # On fait un hello
105
        sock.write(json.dumps(["hello", ["HTTP Django", ip_user]]))
106 107 108
        # On récupère la réponse du hello
        out = full_read(sock)
    except Exception as exc:
109 110 111
        # Erreur de connexion plus explicite en mode debug
        if settings.DEBUG:
            raise
112 113 114 115 116 117 118 119 120 121 122 123 124 125
        # Si on a foiré quelque part, c'est que le serveur est down
        raise NKRefused(str(exc))
    if out["retcode"] == 0:
        return sock
    elif out["retcode"] == 11:
        raise NKHelloFailed(out["errmsg"])
    else:
        raise NKUnknownError(out["errmsg"])

def _gen_redirect_postlogin(request):
    """Génère l'uri de redirection contenant ``"?next=<la page où on veut aller après le login>"``"""
    next_page = re.sub(r"\?.*", "", request.path) # On ne garde pas ce qui était éventuellement présent dans le get.
    return settings.NOTE_LOGIN_URL + "?%s" % (urllib.urlencode({"next" : next_page}),)

126
def gerer_NKError(request, exc):
127 128
    """Fait ce qu'il faut en fonction de l'erreur qui a eu lieu pendant la communication avec le serveur NK2015."""
    if isinstance(exc, NKRefused):
129
        messages.add_error(request, messages.ERRMSG_NK2015_DOWN)
130 131
        return HttpResponseRedirect(_gen_redirect_postlogin(request))
    if isinstance(exc, NKHelloFailed):
132
        messages.add_error(request, messages.ERRMSG_HELLO_FAILED)
133 134
        return HttpResponseRedirect(_gen_redirect_postlogin(request))
    if isinstance(exc, NKDeadServer):
135
        messages.add_error(request, messages.ERRMSG_NK2015_NOT_RESPONDING)
136 137
        return HttpResponseRedirect(_gen_redirect_postlogin(request))
    if isinstance(exc, NKUnknownError):
138
        erreur = messages.ERRMSG_UNKOWNERROR + "\n"
139 140 141 142 143 144 145 146 147 148 149 150
        erreur += str(exc)
        messages.add_error(request, erreur)
        return HttpResponseRedirect(_gen_redirect_postlogin(request))
    else:
        typ = django.utils.html.escape(str(type(exc)))
        s = django.utils.html.escape(str(exc))
        return HttpResponse("La gestion de cette erreur n'est pas prévue :\n%s : %s" % (typ, s))

def login_NK(request, username, password, form, masque=[[], [], False]):
    """Ouvre une connexion au serveur NK2015 par username/password
       Renvoie dans tous les cas un objet HttpResponse[Redirect] utilisable directement"""
    try:
151
        sock = connect_NK(request)
152 153 154 155 156 157
        data = [username, password, "bdd", masque]
        paquet = ["login", data]
        sock.write(json.dumps(paquet))
        out = full_read(sock)
        retcode, errmsg = out["retcode"], out["errmsg"]
    except NKError as exc:
158
        return gerer_NKError(request, exc)
159 160 161 162 163
    if retcode == 0:
        # login réussi
        request.session["logged"] = "ok"
        # On demande au serveur quelles sont les pages auxquelles on a le droit d'accéder
        try:
164
            sock.write(json.dumps(["django_get_accessible_pages", settings.EXISTING_PAGES]))
165 166 167 168 169 170 171
            out = full_read(sock)
            if _is_success_code(out["retcode"]):
                pages = [i for i in out["msg"] if i[0]!="Index"] # On ignore la page d'index, inutile car le lien est déjà là
            else:
                messages.add_error(request, out["errmsg"])
                return HttpResponseRedirect(settings.NOTE_LOGIN_URL)
        except NKError as exc:
172
            return gerer_NKError(request, exc)
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
        save_pages = []
        for p in pages:
            save_pages.append({
                    "name": p[0],
                    "link": p[1],
                    "full_link": "%s%s/" % (settings.NOTE_ROOT_URL, p[1]),
                    })
        request.session["pages"] = save_pages
        sock.write(json.dumps(["whoami"]))
        out = full_read(sock)
        whoami = out["msg"]
        request.session["whoami"] = whoami
        # On conserve la connexion au serveur NK2015
        keep_alive.CONNS[whoami["idbde"]] = sock
        # On redirige vers /index, sauf si on était en train de se reloguer en venant d'un endroit particulier
188
        index_fallback = '%sindex/' % (settings.NOTE_ROOT_URL,)
189 190 191 192 193 194 195 196
        next = request.GET.get("next", index_fallback)
        return HttpResponseRedirect(next)
    else:
        messages.add_error(request, errmsg)
        try:
            del request.session["logged"]
        except: # Si on ne s'est encore jamais logué, la valeur n'existe pas
            pass
197
        variables = basic._fundamental_variables()
198
        variables["form"] = form
199
        return render(request, 'note/login.html', variables)
200 201

def socket_still_alive(request):
202 203 204 205 206 207 208
    """
    Récupère dans :py:mod:`keep_alive` la socket de communication avec le serveur NK2015
    et vérifie que la session est toujours active.

     * En cas de réussite, renvoie ``(True, <la socket de connexion>)``.
     * En cas d'échec, renvoie ``(False, <un objet HttpResponse prêt à l'emploi>)``.
    """
209
    idbde = request.session["whoami"]["idbde"]
210
    # On récupère la socket dans keep_alive.CONNS
211 212 213
    if keep_alive.CONNS.has_key(idbde):
        sock = keep_alive.CONNS[idbde]
    else:
214
        messages.add_error(request, messages.ERRMSG_NOSOCKET)
215
        return (False, HttpResponseRedirect(_gen_redirect_postlogin(request)))
216
    # On vérifie que le serveur NK2015 est toujours d'accord pour nous parler
217 218 219 220
    try:
        sock.write(json.dumps(["mayi", "alive"]))
        out = full_read(sock)
    except NKError as exc:
221
        return (False, gerer_NKError(request, exc))
222 223 224 225 226
    retcode, still_alive = out["retcode"], out["msg"]
    if retcode == 0:
        if still_alive:
            return (True, sock)
        else:
227
            messages.add_error(request, messages.ERRMSG_NK2015_SESSION_EXPIRED)
228 229
    else:
        messages.add_error(request, out["errmsg"])
230
    return (False, HttpResponseRedirect(_gen_redirect_postlogin(request)))