28 марта 2024 г.

Socks-сервер, dante v1.4.2, на debian 12

Шпаргалка для себя, некоторые изменения относительно штатного.
logoutput: /var/log/sockd.log
Чтобы это работало, нужно пофиксить systemd-юнит, в дебиане по дефолту там стоит
ReadOnlyDirectories=/bin /etc /lib -/lib64 /sbin /usr /var
т.е. надо убрать /var, иначе будет в логах старта что-то типа:
danted[130834]: alert: configparsing(): could not (re)open logfile "/var/log/sockd.log": Read-only file system

Авторизация:

24 декабря 2018 г.

django: фильтр нужных разрешений юзера в админке

В админке Django всегда делаю чтобы в настройках профиля и настройках группы светились только нужные, используемые в логике пермишены (например, свои или какие-то отдельные стандартные).
Иначе их обычно слишком много и непонятно какие в реальности имеют смысл.
Просто перегружаются Admin-классы и формы в них, где фильтруется ModelForm.queryset у нужного поля формы.

admin.py
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.forms import UserChangeForm
from django.db.models import Q
from django import forms

USED_PERMS = [
    'reports.add_report',
    'reports.change_report',
    'app2.custom_perm',
]

def qs_perm_filter(qs):
    ups = [up.split('.') for up in USED_PERMS]
    q_expressions = [Q(content_type__app_label=up[0], codename=up[1]) for up in ups]
    return qs.filter(reduce(operator.or_, q_expressions))
    
# наследуем форму (у меня там ещё много помимо этого), также см. нужную model
class UserProfileChangeForm(UserChangeForm):
    class Meta:
        model = UserProfile
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(UserProfileChangeForm, self).__init__(*args, **kwargs)

        f = self.fields.get('user_permissions')
        if f is not None:
            f.queryset = qs_perm_filter(f.queryset)

# наследуем кастомный UserAdmin
class UserProfileAdmin(UserAdmin):
    ...
    form = UserProfileChangeForm
    ...
    
# показываем профиль, родной User не показываем
admin.site.unregister(User)
admin.site.register(UserProfile, UserProfileAdmin)

# для групп тоже аналогично всё
class MyGroupAdminForm(forms.ModelForm):
    class Meta:
        model = Group
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super(MyGroupAdminForm, self).__init__(*args, **kwargs)

        f = self.fields.get('permissions')
        if f is not None:
            f.queryset = qs_perm_filter(f.queryset)


class MyGroupAdmin(GroupAdmin):
    form = MyGroupAdminForm

admin.site.unregister(Group)
admin.site.register(Group, MyGroupAdmin)

Также для бонуса - вынос пермишенов пользователя в таблицу с юзерами в админке. Из списка видно кто и каким правами обладает, там все группы, персональные права, указание на доступ к админке и т.д:

21 ноября 2018 г.

Django не давать логиниться одновременно с двух разных мест

Другими словами, не разрешаются две параллельные сессии. При новом входе старый разлогинивается.
Может понадобиться для какого-нибудь портала в интранете или чего-то такого секюрного.
Решается всё довольно банально, есть специальный сигнал user_logged_in.
Ищем сессию для этого юзера, если нашли - сбрасываем в базе, сбрасываем в текущем engine (если это применимо).
Если что, у меня настроен один из стандартных:
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
Код обработчика сигнала. Всё заполонил комментариями, думаю, там всё понятно.
from django.contrib.auth.signals import user_logged_in
from django.contrib import messages
from django.contrib.sessions.models import Session
from django.utils import timezone

def user_logged_in_handler(sender, request, user, **kwargs):
    # текущая сессия: request.session.session_key
    for session in Session.objects.filter(expire_date__gte=timezone.now()).exclude(session_key=request.session.session_key):
        if session.get_decoded().get('_auth_user_id', None) == str(user.id):
            # нашли какую-то сессию, делаем её expired
            session.expire_date = timezone.now()
            session.save()
            # далее нам надо очистить все данные сессии
            # сначала я пробовал так
            # SessionStore = session.get_session_store_class()
            # sesstor = SessionStore(session.session_key)
            # sesstor.flush()
            # где flush - это метод именно для моего бекенда cached_db
            # я ожидал, что для модели вернётся текущий настроенный в settings.SESSION_ENGINE бекенд, но в модели Session захардкожено (как ни странно?) db.SessionStore
            # в принципе можно напрямую тупо импортировать cached_db.SessionStore, но сделал проще и универсальнее:
            # request.session у нас уже содержит установленный мидлварей нужный SessionStore
            # с помощью "чужой" нашей новой SessionStore удаляем левый наш старый session_key, там удачно есть такой даже метод
            request.session.delete(session.session_key)
            # раз у нас доступен request, то при желании показываем юзеру сообщение
            messages.add_message(request, messages.INFO, 'Была закрыта дублирующаяся сессия на каком-то другом компьютере')

user_logged_in.connect(user_logged_in_handler)

14 апреля 2016 г.

turn-сервер coturn webrtc error 401: Unauthorised

Сервер coturn по дефолту не вполне годный для использования в качестве turn-сервера для webrtc-клиентов. На stun-запросы нормально отвечает, но при попытке использования в качестве turn всё время проблемы в клиенте (не вполне очевидно отлаживаемые), а в логах либо ошибки подключения, либо что-то типа "401: Unauthorised".

WebRtc не станет работать нормально без авторизации с turn-сервером, потому там надо настроить и авторизацию. Используем long-term механизм с предопределённым логином-паролем. Там есть более хитрые механизмы, а также использование ключей в кач-ве паролей итд, но суть задачи не в этом.

в /etc/turnuserdb.conf прописывается логин-пароль:
qwerty:asdfgh

в конфиге /etc/turnserver.conf прописываются/раскоменчиваются минимально необходимые настройки:
# использование fingerprint, обычно webrtc его хочет
fingerprint
# включение long-term авторизации (хотя вроде автоматически должен включаться, если прописан хоть один аккаунт походящий)
lt-cred-mech
# файл с логинами-паролями (можно прописать напрямую в этом же конфиге, но не очень красиво)
userdb=/etc/turnuserdb.conf
# дефолтрый реалм тоже нужно
realm=qwerty
После этого сервер откликается на настройки из webrtc типа
{urls:'turn:IP:3478',username:'qwerty',credential:'asdfgh',}

8 февраля 2016 г.

dlango: autocomplete_light дополнительный рендер в json

Если цель - просто изменить рендер не в готовый html (для родных виджетов приложения), а в другой вид, в том числе json, то всё просто. Здесь же речь о том, чтобы добавить просто новый способ параллельно с полноценно работающим web-способом. В итоге будет и точно так же работающие автодополнения, рендерящие не в html, а в json, и индексная справка по корню, с корректными новыми url, указывающими на наши новые методы из api.

urls.py
from autocomplete_light.views import RegistryView
...
url(r'^api/autocomplete/(?P[-\w]+)/$', views.ApiAutocompleteView.as_view(), name='api_autocomplete_light_autocomplete'),
url(r'^api/autocomplete/$', RegistryView.as_view(template_name='autocomplete_light/api_registry.html'), name='api_autocomplete_light_registry'),
...

Вторая задача (вывод списка зарегистрированных автодополнений) решается вообще без переопределения, используем стандартную RegistryView, которая вполне подходит, нужно только переопределить template_name и в новом шаблоне вызвать get_absolute_url_api вместо get_absolute_url.

api_registry.html
{% if registry|length %}
    

List of your {{ registry_items|length }} registered api-autocompletes

{% for name, autocomplete in registry_items %}

{{ name }}

{{ autocomplete.get_absolute_url_api }}
{% endfor %}
{% else %}

You have not registered any api-autocomplete

{% endif %}

28 сентября 2015 г.

${0%${0##*/}}

Есть небольшой трюк в bash, который мне давно нравился — получение текущей директории запущенного скрипта, используя только $0 и операции над строками bash-а. Это то, что в заголовке. Как вариант, его можно использовать в виде:
cd ${0%${0##*/}}
Исходные данные: $0 - полный путь запущенного скрипта. Понятно, что скрипт надо выполнять по полному пути, иначе использование метода лишено смысла.

Используются последовательно две операции над строками:
${string##substring} - удаление самой длинной, из найденных, подстроки $substring в строке $string. Поиск ведется с начала строки.
${string%substring} - удаление самой короткой, из найденных, подстроки $substring в строке $string. Поиск ведется с конца строки.


${0%${0##*/}} - самая длинная из найденных строк */ — это весь путь до последнего слеша включительно. Если удалить это из полного пути, то получится просто имя файла самого скрипта (без пути).

${0%${0##*/}} - далее, если из $0 («полный путь») удалить «имя файла» (получено выше), то получится как раз «путь без имени файла».

5 марта 2014 г.

django: самодельный if-else тег

Здесь пример самодельного тега шаблонизатора django, выполняющего роль if-else-endif. Мой код подразумевает переменную 'current_statuskey' в контексте (кладётся напрямую или с помощью middleware). Писал для очень сложной страницы с кучей частей, имеющих разный вид в зависимости от статуса. Вообще там ещё у меня дополнительная логика, но это минимальный пример здесь. Использование в шаблоне:
{% ifstatus one,five,blabla %}
...
{% else %}
...
{% endifstatus %}
Можно было описать в десяток строк, но здесь больше и код очень сильно похож на стандартные теги ifequals и сопутствующие, откуда я его и адаптировал, чтобы получить более правильную и канонiчную реализацию.

4 февраля 2014 г.

ant: компиляция и сборка jar прямиком с git

Пояснять особо нечего тут. Используются только стандартные таски. Для git clone используется exec, далее компилируется и собирается как обычно. Ниже пример моего ant-скрипта для сборки с git либы json.jar.

15 декабря 2013 г.

проект django project на shared hosting хостинге mod_wsgi

Историческая заметка по большей части. Черновик схемы настройки проекта на django 1.4 на хостинге с mod_wsgi.

13 ноября 2013 г.

django: contrib.comments работаем через ajax

Тут рассказ о том, как заставить стандартный django.contrib.comments работать через ajax. Цель была такова: минимальные изменения в клиентских view, чтобы работали стандартные templates перегружаемые, полноценная админка (обычная от приложения comments), да и вообще максимальное использование оригинального кода. Короче, в итоге после нескольких самопроизвольных переписываний (вместе с использованием подобного решения в проектах) получилась минимальная обёртка, которую по идее тоже можно вынести в отдельный app.

Да, скоро это уже не совсем актуально станет (django.contrib.comments устаревший и с версии 1.6 вынесен в отдельный проект), но решение отточилось на третьей версии и мне жалко его терять просто :) Пример тут для версии 1.5.

2 ноября 2013 г.

django: текущий пользователь в логах

Удобно в логах иметь текущего залогиненого юзера, чтобы видно было на ком падает, удобнее разбираться. Для логгера "django.request" передаётся экстра-параметр request. Его можно вытащить и залоггировать. Например, с помощью кастомного фильтра. Фильтр добавляет в вывод форматтера поле "user" с именем (строковое представление#id) юзера если оно есть. Фильтр вешается на нужные handlers. А в соответствующие formatters на этих handlers добавляется поле "user". В фильтре проверка на существование атрибутов request и request.user обязательна, если текущий formatter повешен на что-либо, кроме логгера "django.request", иначе будет падать в попытке найти поле с ид "user".

class RequestPushUserFilter(logging.Filter):
    def filter(self, record):
        if hasattr(record, 'request'):
            if hasattr(record.request, 'user'):
                record.user = u'%s#%s' % (record.request.user, record.request.user.pk)
            else:
                record.user = '?'
        else:
            record.user = '-'
        return True


LOGGING = {
    ...
    'filters': {
        ...
        'request_pushuser_filter': {
            '()': 'project.settings.RequestPushUserFilter',
        },
    },
    ...
    'formatters': {
        'standard': {
            'format': "[%(asctime)s] %(levelname)s [%(name)s:%(module)s:%(lineno)s] [%(user)s] %(message)s",
            'datefmt': '%d.%m.%Y %H:%M:%S',
        },
    },
    ...
    'handlers': {
        'logfile': {
            ...
            'filters': ['request_pushuser_filter'],
            ...
            'formatter': 'standard',
        },
    ...
В итоге в логах будет примерно так:
[31.10.2013 00:43:20] ERROR [django.request:base:212] [Driver John#1] Internal Server Error: блабла

См. также про логгирование в файл django.

31 октября 2013 г.

django: csrf при ajax-запросах

Несколько раз встречал вопросы на счёт csrf при самодельных ajax-запросах (например, через jquery как в примере ниже), и несколько вариантов странных даже видел. Я делаю проще - в основную страницу (например, в шаблон base.html или как он там у вас назван) выношу стандартный csrf_token в качестве javascript-переменной и во всех последующих скриптах удобно его использую.
Секюрность не страдает, конечно, т.к. сам токен не представляет интереса. Потом при запросах, например, через jquery.ajax можно просто передавать как параметр с именем "csrfmiddlewaretoken" (аналогично тому как он передаётся через скрытый input в обычных формах)

$.ajax({
  type: 'POST',
  url: '...',
  data : { ..., 'csrfmiddlewaretoken' : csrf_token, },
  ...
});

21 октября 2013 г.

linux: русский в консоли archlinux

Возникла проблема со шрифтами в виртуальной консоли. Квадратики вместо кириллицы. В графическом эмуляторе терминала, разумеется, всё нормально, а в /dev/ttyX беда. Выяснилось, что всё портит systemd, сначала загружая шрифты и настраивая их согласно vconsole.conf как и положено, а потом подгружая drm-модуль видеокарты, который создаёт новый фреймбуфер (например, у меня /dev/fb0), в котором уже никаких настроек не делается.

7 октября 2013 г.

django: логгирование в файл и перехват warnings

В django иногда удобно вести файловые логи в приложении. По умолчанию настроен только один handler — mail_admins (отправка на мыло админам), что не всегда хорошо, а иногда невозможно (в принципиальном интранете, например). Вторая задача возникла — хорошо бы отлавливать туда же и все варнинги из warnings.warn, которые в том числе и django иногда вываливает.
Первая задача тривиальна и описана в документации. Создаём новый handler RotatingFileHandler и добавляем в нужные логгеры (очевидно, что речь про settings.py):
LOGGING = {
...
    'handlers': {
...
        'logfile': {
            'level': 'WARNING',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': rel('log', 'logfile.log'),
            'maxBytes': 1000000,
            'backupCount': 666,
            'formatter': 'standard',
        },
...
    },
    'loggers': {
        'django.request': {
            'handlers': ['mail_admins', 'logfile'],
            'level': 'WARNING',
            'propagate': True,
        },
...
    },
}
вторая задача тоже просто решается. Делается
import logging
logging.captureWarnings(True)
После этого все варнинги начинаются писаться также в логгер с именем "py.warnings", откуда их и логируем:
...
    'loggers': {
...
        'py.warnings': {
            'handlers': ['console', 'logfile'],
            'level': 'WARNING',
            'propagate': True,
        },
...
    },

6 октября 2013 г.

django: удобные относительные пути в settings

В settings-файле джанги удобно придумать какой-то порядок, потому что рутинных записей всяких копится целая куча. Например, для работы с путями (которые почти все относительны) использую небольшой трюк.
import os

# путь корня этого проекта (там где manage лежит)
PROJECT_DEPLOY_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))

rel = lambda *path: os.path.join(PROJECT_DEPLOY_PATH, *path)
Без лямбды функция rel может выглядеть как-то так:
def rel(*path):
    return os.path.join(PROJECT_DEPLOY_PATH, *path)
И далее просто используется:
STATIC_ROOT = rel('static')
Или для путей из нескольких подпапок (/static/files/)
STATIC_ROOT = rel('static','files')

21 сентября 2013 г.

django: нормальные bootstrap3 инпуты в autocomplete_light

В bootstrap 3 поля автозаполнения от django-модуля autocomplete_light выглядят непотребно из-за требования иметь красивым инпутам формы обязательный класс "form-control". Никаких возможностей кастомизации через autocomplete_light_registry.py и т.п. нету, т.к. класс намертво захардкожен (widget.html):
{% block input %}
    {# a text input, that is the 'autocomplete input' #}
    
{% endblock %}
Пришлось сделать патчик в js и всем полям с class="autocomplete" добавить ещё и класс "form-control" (используется jquery):
if ($(".autocomplete").length) {
 $(".autocomplete").addClass( "form-control" );
}
Если используется другой вариант хардкода (типа насильное назначение вообще всем input, либо хардкодом в css), то неактуально. Я люблю чистые решения, но чище этого ничего не смог придумать.

16 сентября 2013 г.

swt jface button dropdown popup menu

Хочу я, чтобы при нажатии на кнопку вываливалось меню. Ну типа как Start-меню в винде. Всё очень просто - по нажатию кнопки создаём и разворачиваем в месте тыкания мышкой.
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Menu;
...
final Button mainMenuButton = new Button(composite, SWT.PUSH);
mainMenuButton.setText("¿?");
mainMenuButton.setLayoutData( ... );
mainMenuButton.addSelectionListener(new SelectionAdapter() {
    @Override
    public void widgetSelected(SelectionEvent event) {
        Menu dropMenu = createMainMenu( getShell() );
        Point point = mainMenuButton.toDisplay(new Point(event.x, event.y));
        dropMenu.setLocation( point.x, point.y+mainMenuButton.getBounds().height );
        dropMenu.setVisible(true);
    }
});
Далее на чистом SWT как-то так:
public static Menu createMainMenu( final Shell shell )
{
    Menu dropMenu = new Menu(shell, SWT.POP_UP);
    ...
    MenuItem item0 = new MenuItem(dropMenu, SWT.PUSH);
    item0.setText(...);
    item0.addListener(SWT.Selection, ...);
    ....
    return dropMenu;
}
На JFace как-то так:
public Menu createMainMenu( final Shell shell )
{
    MenuManager popManager = new MenuManager();
    popManager.add(new ActionOne(shell));
    ...
    popManager.add(new Separator());
    ...
    Menu dropMenu = popManager.createContextMenu(shell);
    return dropMenu;
}

29 августа 2013 г.

django upload_to windows 123 bad chars

Использовал свою реализацию upload_to-метода для формирования пути сохранения файлов в FileField-поле модели на основании заголовка сущности (чтобы файлики сохранялись в подпапки контрагентов). При разработке под linux всё было отлично, но в продакшене на одной windows-машине периодически валилась загрузка файлов
File "C:\python27\lib\site-packages\django\core\files\storage.py", line 168, in _save
    os.makedirs(directory)
  File "C:\python27\lib\os.py", line 150, in makedirs
    makedirs(head, mode)
  File "C:\python27\lib\os.py", line 157, in makedirs
    mkdir(name, mode)
WindowsError: [Error 123] Синтаксическая ошибка в имени файла,: u'C:\\inetpub\\wwwroot\\ZooDjangoProject\\media\\file\\\blabla "blabla"'
Выяснилось, что дело в кавычках в имени файлов, который есть запретный символ в винде. Пришлось вспомнить и остальные ограничения, после чего родилось прижившееся экспресс-решение.
def removebadchars(value):
    for c in r'\/:*?"<>|':
        value = value.replace(c, '')
    return value
...
filename = removebadchars(filename)

27 августа 2013 г.

Версия приложения через makefile прямиком из git

Так как код я держу в той или иной VCS (последнее время чаще всего в git) я подумал, что было бы круто версию приложений брать из репозитория, а конкретно — из тегов. Для небольших утилит особенно удобно. Некоторые похожие решения из интернета натолкнули меня на такое решение. Всё довольно просто, пример для обычного gnu-makefile и исходника на Си.

26 августа 2013 г.

django admin запрет редактирования модели

Запрещение в админке django редактирования какой-либо сущности. В моём случае это оповещения об оплатах платёжной системы. В ModelAdmin есть методы has_add_permission, has_change_permission, has_delete_permission с очевидным предназначением. Правда, если все они вернут в каком-то случае False, то модель вообще не отобразится в списке сущностей админки и по прямой ссылке тоже не будет работать. Так что все поля вместо has_change_permission надо сделать readonly.
class SuccessNotificationAdmin(admin.ModelAdmin):
    ...
    readonly_fields = ('order', 'sum', )

    def has_add_permission(self, request):
        return False

    #def has_change_permission(self, request, obj=None):
    #    return False

    def has_delete_permission(self, request, obj=None):
        return False

admin.site.register(SuccessNotification, SuccessNotificationAdmin)