Projet

Général

Profil

0001-contrib-add-a-shellinabox-plugin.patch

Benjamin Dauvergne, 16 octobre 2015 14:08

Télécharger (410 ko)

Voir les différences:

Subject: [PATCH] contrib: add a shellinabox plugin

 passerelle/contrib/shellinabox/README       |  11 +++
 passerelle/contrib/shellinabox/__init__.py  |  27 ++++++
 passerelle/contrib/shellinabox/shellinaboxd | Bin 0 -> 772688 bytes
 passerelle/contrib/shellinabox/urls.py      |  25 +++++
 passerelle/contrib/shellinabox/views.py     | 143 ++++++++++++++++++++++++++++
 5 files changed, 206 insertions(+)
 create mode 100644 passerelle/contrib/shellinabox/README
 create mode 100644 passerelle/contrib/shellinabox/__init__.py
 create mode 100755 passerelle/contrib/shellinabox/shellinaboxd
 create mode 100644 passerelle/contrib/shellinabox/urls.py
 create mode 100644 passerelle/contrib/shellinabox/views.py
passerelle/contrib/shellinabox/README
1
Shellinabox for Passerelle
2
==========================
3

  
4
How to use
5
----------
6

  
7
1) Add to your settings.py
8

  
9
        # local_settings.py:
10
        INSTALLED_APPS += ('passerelle.contrib.shellinabox',)
11
        PASSERELLE_APP_SHELLINABOX_ENABLED = True
passerelle/contrib/shellinabox/__init__.py
1
# passerelle.contrib.seisin_by_email
2
# Copyright (C) 2015  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import django.apps
18

  
19
class AppConfig(django.apps.AppConfig):
20
    name = 'passerelle.contrib.shellinabox'
21
    label = 'shellinabox'
22

  
23
    def get_after_urls(self):
24
        from . import urls
25
        return urls.urlpatterns
26

  
27
default_app_config = 'passerelle.contrib.shellinabox.AppConfig'
passerelle/contrib/shellinabox/urls.py
1
# passerelle.contrib.seisin_by_email
2
# Copyright (C) 2015  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import patterns, url
18
from passerelle.urls_utils import required, app_enabled
19

  
20
from .views import shellinabox
21

  
22
urlpatterns = required(
23
    app_enabled('shellinabox'),
24
    patterns('', url(r'^shellinabox(/.*)$', shellinabox))
25
)
passerelle/contrib/shellinabox/views.py
1
# passerelle.contrib.seisin_by_email
2
# Copyright (C) 2015  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import fcntl
18
import os
19
import time
20

  
21
import requests
22

  
23
from django.views.decorators.csrf import csrf_exempt
24
from django.http import HttpResponse
25
from django.http import QueryDict
26
from django.core.exceptions import PermissionDenied
27

  
28
LOCKFILE = '/tmp/shellinabox.lock'
29
PIDFILE = '/tmp/shellinabox.pid'
30
SHELLINABOXD = os.path.join(os.path.dirname(__file__), 'shellinaboxd')
31

  
32
@csrf_exempt
33
def shellinabox(request, path):
34
    if not request.user.is_authenticated() or not request.user.is_superuser:
35
        raise PermissionDenied
36
    while not os.path.exists(PIDFILE):
37
        with open(LOCKFILE, 'w') as lockfile:
38
            try:
39
                fcntl.lockf(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
40
            except IOError:
41
                time.sleep(1)
42
                continue
43
            else:
44
                # launch shell in a box
45
                newpid = os.fork()
46
                if newpid:
47
                    time.sleep(1)
48
                    pid, errcode = os.waitpid(newpid, os.WNOHANG)
49
                    if pid != 0:
50
                        return HttpResponse('failure to launch shellinaboxd: %r' % errcode)
51
                    with file(PIDFILE, 'w') as pidfile:
52
                        pidfile.write(str(newpid))
53
                else:
54
                    uid = os.getuid()
55
                    gid = os.getgid()
56
                    os.execv(SHELLINABOXD, [SHELLINABOXD, '-d', '-t', '-s', '/:%s:%s:HOME:/bin/bash' % (uid, gid)])
57
            finally:
58
                fcntl.lockf(lockfile.fileno(), fcntl.LOCK_UN)
59
    return proxy_view(request, 'http://localhost:4200%s' % path)
60

  
61
def proxy_view(request, url, requests_args=None):
62
    """
63
    Forward as close to an exact copy of the request as possible along to the
64
    given url.  Respond with as close to an exact copy of the resulting
65
    response as possible.
66
    If there are any additional arguments you wish to send to requests, put
67
    them in the requests_args dictionary.
68
    """
69
    requests_args = (requests_args or {}).copy()
70
    headers = get_headers(request.META)
71
    params = request.GET.copy()
72

  
73
    if 'headers' not in requests_args:
74
        requests_args['headers'] = {}
75
    if 'data' not in requests_args:
76
        requests_args['data'] = request.body
77
    if 'params' not in requests_args:
78
        requests_args['params'] = QueryDict('', mutable=True)
79

  
80
    # Overwrite any headers and params from the incoming request with explicitly
81
    # specified values for the requests library.
82
    headers.update(requests_args['headers'])
83
    params.update(requests_args['params'])
84

  
85
    # If there's a content-length header from Django, it's probably in all-caps
86
    # and requests might not notice it, so just remove it.
87
    for key in headers.keys():
88
        if key.lower() == 'content-length':
89
            del headers[key]
90

  
91
    requests_args['headers'] = headers
92
    requests_args['params'] = params
93
    if not requests_args['params']:
94
        del requests_args['params']
95

  
96
    response = requests.request(request.method, url, **requests_args)
97

  
98
    proxy_response = HttpResponse(
99
        response.content,
100
        status=response.status_code)
101

  
102
    excluded_headers = set([
103
        # Hop-by-hop headers
104
        # ------------------
105
        # Certain response headers should NOT be just tunneled through.  These
106
        # are they.  For more info, see:
107
        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
108
        'connection', 'keep-alive', 'proxy-authenticate', 
109
        'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 
110
        'upgrade', 
111

  
112
        # Although content-encoding is not listed among the hop-by-hop headers,
113
        # it can cause trouble as well.  Just let the server set the value as
114
        # it should be.
115
        'content-encoding',
116

  
117
        # Since the remote server may or may not have sent the content in the
118
        # same encoding as Django will, let Django worry about what the length
119
        # should be.
120
        'content-length',
121
    ])
122
    for key, value in response.headers.iteritems():
123
        if key.lower() in excluded_headers:
124
            continue
125
        proxy_response[key] = value
126

  
127
    return proxy_response
128

  
129

  
130
def get_headers(environ):
131
    """
132
    Retrieve the HTTP headers from a WSGI environment dictionary.  See
133
    https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.META
134
    """
135
    headers = {}
136
    for key, value in environ.iteritems():
137
        # Sometimes, things don't like when you send the requesting host through.
138
        if key.startswith('HTTP_') and key != 'HTTP_HOST':
139
            headers[key[5:].replace('_', '-')] = value
140
        elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
141
            headers[key.replace('_', '-')] = value
142

  
143
    return headers
0
-