Projet

Général

Profil

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

Benjamin Dauvergne, 24 mars 2017 16:40

Télécharger (9,78 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/urls.py     |  25 +++++
 passerelle/contrib/shellinabox/views.py    | 160 +++++++++++++++++++++++++++++
 4 files changed, 223 insertions(+)
 create mode 100644 passerelle/contrib/shellinabox/README
 create mode 100644 passerelle/contrib/shellinabox/__init__.py
 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 = None
31

  
32
for bindir in ['/usr/bin/', '/usr/sbin/', '/usr/local/bin/', '/usr/local/sbin']:
33
    path = os.path.join(bindir, 'shellinaboxd')
34
    if os.path.exists(path):
35
        SHELLINABOXD = path
36
        break
37

  
38

  
39
@csrf_exempt
40
def shellinabox(request, path):
41
    if not SHELLINABOXD:
42
        return HttpResponse('shellinabox is not available',
43
                            status_code=500)
44

  
45
    if not request.user.is_authenticated() or not request.user.is_superuser:
46
        raise PermissionDenied
47
    while not os.path.exists(PIDFILE):
48
        with open(LOCKFILE, 'w') as lockfile:
49
            try:
50
                fcntl.lockf(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
51
            except IOError:
52
                time.sleep(1)
53
                continue
54
            else:
55
                # launch shell in a box
56
                newpid = os.fork()
57
                if newpid:
58
                    time.sleep(1)
59
                    pid, errcode = os.waitpid(newpid, os.WNOHANG)
60
                    if pid != 0:
61
                        return HttpResponse('failure to launch shellinaboxd: %r' % errcode)
62
                    with file(PIDFILE, 'w') as pidfile:
63
                        pidfile.write(str(newpid))
64
                else:
65
                    uid = os.getuid()
66
                    gid = os.getgid()
67
                    os.execv(SHELLINABOXD, [SHELLINABOXD, '-d', '-t', '-s',
68
                                            '/:%s:%s:HOME:/bin/bash' % (uid, gid)])
69
            finally:
70
                fcntl.lockf(lockfile.fileno(), fcntl.LOCK_UN)
71
    return proxy_view(request, 'http://localhost:4200%s' % path)
72

  
73

  
74
def proxy_view(request, url, requests_args=None):
75
    """
76
    Forward as close to an exact copy of the request as possible along to the
77
    given url.  Respond with as close to an exact copy of the resulting
78
    response as possible.
79
    If there are any additional arguments you wish to send to requests, put
80
    them in the requests_args dictionary.
81
    """
82
    if not SHELLINABOXD:
83
        return HttpResponse('shellinabox is not available',
84
                            status_code=500)
85

  
86
    requests_args = (requests_args or {}).copy()
87
    headers = get_headers(request.META)
88
    params = request.GET.copy()
89

  
90
    if 'headers' not in requests_args:
91
        requests_args['headers'] = {}
92
    if 'data' not in requests_args:
93
        requests_args['data'] = request.body
94
    if 'params' not in requests_args:
95
        requests_args['params'] = QueryDict('', mutable=True)
96

  
97
    # Overwrite any headers and params from the incoming request with explicitly
98
    # specified values for the requests library.
99
    headers.update(requests_args['headers'])
100
    params.update(requests_args['params'])
101

  
102
    # If there's a content-length header from Django, it's probably in all-caps
103
    # and requests might not notice it, so just remove it.
104
    for key in headers.keys():
105
        if key.lower() == 'content-length':
106
            del headers[key]
107

  
108
    requests_args['headers'] = headers
109
    requests_args['params'] = params
110
    if not requests_args['params']:
111
        del requests_args['params']
112

  
113
    response = requests.request(request.method, url, **requests_args)
114

  
115
    proxy_response = HttpResponse(
116
        response.content,
117
        status=response.status_code)
118

  
119
    excluded_headers = set([
120
        # Hop-by-hop headers
121
        # ------------------
122
        # Certain response headers should NOT be just tunneled through.  These
123
        # are they.  For more info, see:
124
        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
125
        'connection', 'keep-alive', 'proxy-authenticate', 
126
        'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 
127
        'upgrade', 
128

  
129
        # Although content-encoding is not listed among the hop-by-hop headers,
130
        # it can cause trouble as well.  Just let the server set the value as
131
        # it should be.
132
        'content-encoding',
133

  
134
        # Since the remote server may or may not have sent the content in the
135
        # same encoding as Django will, let Django worry about what the length
136
        # should be.
137
        'content-length',
138
    ])
139
    for key, value in response.headers.iteritems():
140
        if key.lower() in excluded_headers:
141
            continue
142
        proxy_response[key] = value
143

  
144
    return proxy_response
145

  
146

  
147
def get_headers(environ):
148
    """
149
    Retrieve the HTTP headers from a WSGI environment dictionary.  See
150
    https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.META
151
    """
152
    headers = {}
153
    for key, value in environ.iteritems():
154
        # Sometimes, things don't like when you send the requesting host through.
155
        if key.startswith('HTTP_') and key != 'HTTP_HOST':
156
            headers[key[5:].replace('_', '-')] = value
157
        elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
158
            headers[key.replace('_', '-')] = value
159

  
160
    return headers
0
-