Category: Go

20
Сен
2022

📰 Weekly #18: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: ИИ-самоучка демонстрирует сходство с тем, как работает мозг; почему размер вашего веб-сайта не должен превышать 14 Кб; почему команда Google Cloud любит Go; руководство по тестированию безопасности iOS-приложений.

31
Июл
2022

🏃 Горутины: что такое и как работают

Легковесная, потребляет мало памяти, имеет низкую задержку — знакомимся с горутиной.

Язык Go, имеющий встроенную поддержку параллельного программирования, представляет вашему вниманию легковесные потоки, выполняющиеся в фоновом режиме. В отличие от потоков, существующих в большинстве других языков — они дешевле с точки зрения занимаемой памяти, межпотокового взаимодействия, имеют низкую задержку и быстрое время запуска. Хотите детальное описание этой сущности — читайте статью!

Определение потока в программировании

Любое работающее приложение, с технической точки зрения — это процесс или, скорее, последовательное выполнение набора инструкций. Эти инструкции исполняются по порядку: сначала первая, потом вторая, затем следующая и так далее. Этот процесс имеет начало и конец.

Отдельный поток подобен процессу и также имеет начало, определенную последовательность действий и конец. Но, в отличие от процесса, поток не является программой и не может работать сам по себе. Вместо этого, он выбирает часть из этой последовательности действий программы и выполняют ее как отдельное приложение. Реальное преимущество этой концепции заключается в том, что потоков может быть несколько, и каждый может одновременно с другими выполнять свою задачу в одной программе.

Простым примером многопоточности является веб-браузер. Ведь в нем можно одновременно загружать файлы, прокручивать страницы вниз, что-то печатать и слушать музыку. Технически, поток можно назвать облегченным процессом, ведь он требует меньше памяти, чем процесс в многопроцессорной среде. Linux обычно обобщает их, называя задачами (они могут быть как процессами, так и потоками).

Однако, между ними все же есть различие.

Процесс — часть выполняемой программы, с выделенными специально для него системными ресурсами (процессорное время, память и т. д.).

Поток же — это своего рода способ выполнения этого процесса.

Что такое Горутины?

Горутины — это дальнейшее усовершенствование концепции потока, а если сказать проще, то это функции, способные работать параллельно с другими такими же функциями в одном адресном пространстве. Причем их настолько хорошо усовершенствовали, что они стали отдельной сущностью. В многопроцессорной среде создание и обслуживание процесса сильно зависят от базовой операционной системы. Процессы потребляют ресурсы операционки и не делят их между узлами. Потоки, хотя и легче, чем процессы, из-за совместного использования ресурсов (между одноранговыми потоками), требуют большого размера стека — почти 1 МБ. Причем стек нужно умножать на количество потоков.

Кроме того, их переключение требует восстановления регистров, таких как программные счетчики, указатели стека, регистры с плавающей запятой и т. д. Из-за этого стоимость обслуживания процесса или потока довольно высока. Кроме того, в случаях, когда данные совместно используются одноранговыми узлами, возникают дополнительные затраты на синхронизацию данных. Хотя накладные расходы на переключение между задачами максимально оптимизированы, постановка новых задач по-прежнему требует больше ресурсов. Иногда это сильно снижает производительность приложения, даже если потоки обозначены как легковесные.

Преимущество горутин в том, что они не зависят от базовой операционной системы, а скорее, существуют в виртуальном пространстве среды выполнения Go. В результате любая оптимизация горутины меньше зависит от платформы, на которой она работает. Они начинают работать с начальной емкости стека размером всего 2-4 КБ и наряду с каналами поддерживают модели параллелизма взаимодействующих последовательных процессов (CSP), где значения передаются между независимыми действиями. Эти действия, как вы уже догадались, называются горутинами.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Как создать горутину в Go

Разработчики должны понимать, что горутины превосходят потоки только количественно. Качественно они одинаковы. При запуске программы в Golang первая горутина вызывает основную функцию и из-за этого ее часто называют основной горутиной. Если же мы захотим создать другие, новые горутины, мы должны использовать оператор go. Например, чтобы вызвать функцию в Golang мы пишем так:

        myfunc()
    

Здесь, после того как ее вызвали, мы опять вернемся к точке вызова. А теперь мы напишем:

        go myfunc()
    

Префикс go вызывает функцию в новой горутине и она (функция) будет выполняться асинхронно с вызвавшим ее участком кода. И если примерно брать в среднем на одну горутину по 4 Кб емкости стека, то имея оперативную память 4Gb, мы сможем создать их около 800 000.

Однако злоупотреблять с ними не стоит, ведь полезны они будут только в следующих случаях:

  1. Когда нам необходима асинхронность. Например, при работе с сетью, дисками, базами данных и т. д.
  2. При большом времени выполнения функции, когда мы можем что-то выиграть, нагрузив другие ядра.

Среда выполнения Go, работающая в фоновом режиме, начинает запуск набора горутин, с помощью планировщика, распределяющего их по машинам. Затем он создает поток, обрабатывающий все горутины, а максимум определяется переменной GOMAXPROCS.

Из документации Go: «Переменная GOMAXPROCS ограничивает количество потоков операционной системы, которые могут одновременно выполнять код Go на уровне пользователя. Количество потоков, которые можно заблокировать в системных вызовах от имени кода Go, не ограничено.

Давайте рассмотрим простой пример для лучшего понимания работы горутин:

        func main() {
	i:= 10
	go fmt.Printf("1. Значение переменной i равно %d\n", i)
	i++
	go fmt.Printf("2. Значение переменной i равно %d\n", i)
	go func() {
	i++
	go fmt.Printf("3. Значение переменной i равно %d\n", i)
	}()
	i++
	go fmt.Printf("4. Значение переменной i равно %d\n", i)
	time.Sleep(1000000)
}
    

Запуск приведенного выше кода в вашем редакторе приведет к следующему результату:

        4. Значение переменной i равно 12
3. Значение переменной i равно 13
1. Значение переменной i равно 10
2. Значение переменной i равно 11

    

Ниже приведен тот же код, только без использования горутин:

        func main() {
	i:= 10
	fmt.Printf("1. Значение переменной i равно %d\n", i)
	i++
	fmt.Printf("2. Значение переменной i равно %d\n", i)
	func() {
	i++
	fmt.Printf("3. Значение переменной i равно %d\n", i)
	}()
	i++
	fmt.Printf("4. Значение переменной i равно %d\n", i)
	time.Sleep(1000000)
}
    

Здесь ожидаемый результат будет следующим:

        1. Значение переменной i равно 10
2. Значение переменной i равно 11
3. Значение переменной i равно 12
4. Значение переменной i равно 13

    

Когда мы ставим перед любой функцией ключевое слово go, оно создает новую горутину и планирует ее выполнение. Идея и поведение такой сущности полностью идентичны потокам: она имеет полный доступ к аргументам, глобальным переменным и остальным доступным элементам кода. Кроме того, мы можем писать горутины с анонимными функциями. Если вы удалите вызов sleep() в первом примере, вывод не будет показан. Следовательно, вызов функции в конце main приведет к тому, что основная горутина остановится и выйдет до того, как порожденная ею горутина получит какой-либо шанс произвести вывод.

Вывод второй части нашего примера довольно прост и последователен, чего нельзя сказать о выводе первой части. Это связано с тем, что компилятор ограничивает порядок доступа к памяти в случае одновременного выполнения горутин и в таком случае невозможно предсказать порядок выводимых строк.

***

Если подытожить, то можно сказать, что горутины — это эффективное ресурсосберегающее средство достижения многозадачности в языке программирования Go. Более подробную информацию по их грамотному использованию можно получить на официальном сайте языка.

Материалы по теме

26
Июл
2022

📰 Weekly #10: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C#, C/C++ и мобильной разработке

В этом выпуске: формы в Джанго 4.0+; как Spotify использует семантический поиск для подкастов; предотвращение утечек памяти в Go; любопытные трюки по работе со строками в C#; полный гайд по SwiftUI Grid и еще 180 полезных ссылок.

28
Июн
2022

📰 Weekly #6: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C# и C/C++

В этом выпуске: базовый и полнотекстовый поиск с помощью Django и Postgres; 15 вопросов по науке о данных, которые вы стесняетесь задать; ответы на популярные вопросы о конкурентности в Go; разработка клона Redis на C#; введение в 30 наиболее важных структур данных и алгоритмов и еще 150 полезных ссылок.

Python

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»

Статьи и руководства

Data Science

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека data scientist’а»

Статьи и руководства

Go

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Новости

Статьи и руководства

  • Веб-разработчик с более чем двадцатилетним опытом помимо основного языка (PHP) решил изучить Go. Полезные заметки и мысли он оставляет у себя в блоге. Вдруг кто-то найдёт в них что-то новое и интересное. Прошёл 13-й день.
  • Вышла вторая часть серии статей с вопросами и ответами на собеседовании по Go.
  • Гайд по использованию Protocol Buffers и gRPC для проектирования API в Go-приложении.
  • Погружение в микросервисы: мощный цикл статей из далекого 2015 года в блоге Nginx:

Introduction to Microservices

Building Microservices: Using an API Gateway

Building Microservices: Inter-Process Communication in a Microservices Architecture

Service Discovery in a Microservices Architecture + пример на Go

Event-Driven Data Management for Microservices

Choosing a Microservices Deployment Strategy

Refactoring a Monolith into Microservices

  • Разбираемся с рендерингом и позиционированием текста. Полезная заметка о разработке игр на Go + ebiten от Искандера, которая сэкономит пару-тройку вечеров.
  • Обзор проблем с существующими SQL-библиотеками в Go и анонс библиотеки, которая стремится упростить их решение.
  • Серия видеоуроков по разработке игры с использованием Go и Raylib. Часть: 5
  • Специалисты Akamai зафиксировали новый P2P-ботнет, разработанный на Go. Что тут сказать? Отличная работа как разработчиков малвари, так и исследователей. Почитайте статью — сами все поймёте.
  • Серия статей по разработке интернет-магазина. В первой части описаны теоретические и практические аспекты разработки и деплоя ориентированного на DDD микросервиса с использованием Go, gRPC, OpenAPI, Docker и других инструментов.
  • Руководство по реализации распределенной трассировки в Go-приложении с использованием OpenTelemetry и SigNoz:.
  • Ответы на популярные вопросы о конкурентности в Go.
  • Как на самом деле устроен тип Map в Go? Рассказывает Николай Тузов:
  • gRPC в действии: разбираем 4 метода взаимодействия с gRPC в Go.
  • Create Your Tests Easily: анонс open source библиотеки от Ozon Tech в BDD-стиле, которая облегчает тяготы создания автотестов.
  • Опыт создания CI-пайплайна для Go-библиотеки с использованием Dagger.
  • Введение в фаззинг: новое видеоруководство на официальном YouTube-канале:
  • Опыт использования Preact, Vite и Hugo для быстрой и эффективной веб-разработки.
  • Uber использует монорепозиторий для внутренних сервисов и библиотек на основе Go. Его недостатком был каскад проверок и сложность при внесении изменений в широко используемые зависимости. В статье команда Uber описывает, как получилось преодолеть эти проблемы.
  • Свежий взгляд на чистую архитектуру в Go от канала The Art of Development:

Библиотеки

  • scc — инструмент для подсчета строк кода в кодовой базе. Работает быстро, оценивает сложность кода, время и необходимое количество разработчиков с общей стоимость реализации проекта с нуля. Интересно узнать, сойдутся ли цифры на ваших проектах?
  • HTTPLoot — Go-инструмент для исследования безопасности веб-приложений. Позволяет автоматически сканировать, определять технический стек, заполнять формы, извлекать секреты из страниц ошибок/отладки и JavaScript-кода сайтов. Инструмент разработан командой RedHunt Labs. Свою результативность он уже доказал, найдя 1 676 634 секретов в результате двух этапов глубокого тестирования.
  • SyMon — легковесный Go-инструмент для системного мониторинга и оповещения.

Книги

Everyday Golang – The Fast Track (2021), Автор: Alex Ellis
Everyday Golang – The Fast Track (2021), Автор: Alex Ellis

Перед вами мини-книга от автора множества руководств по модульному тестированию в Go и других учебных материалов (книги Serverless For Everyone Else и курса Kubernetes on Raspberry Pi).

Everyday Golang — это ускоренный курс для изучения концепций и методов, которые вы можете применять в своей повседневной работе и проектах.

Материал книги представляет собой сборник практических примеров, уроков и техник для Go-разработчиков.

C#

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека шарписта»

Новости

Свежие новости и статьи из мира .NET-разработки:

.NET-документация: новые возможности за май 2022 года:

Статьи и руководства

  • Краткое введение в DiagnosticSource от Эндрю Лока: назначение, сравнение с ILogger / EventSource и практика использования в ASP.NET Core.
  • Разработка клона Redis на C#, шаг за шагом повышая производительность.
  • Теоретический разбор задач (без кода) контеста для разработчиков C#, iOS и Android, который прошёл в рамках отбора участников на курсы Route 256 от Ozon.
  • Немного новостей и практики работы с платформой .NET nanoFramework. В статье показано, как подключить OLED дисплей на контроллере SSD1306, доработать драйвер под новую модификацию дисплея, отправить код в upstream и написать небольшой сканер Wi-Fi сетей.
  • Интерфейс IEnumerable в C#: введение и полезные советы по использованию.
  • Разработка графического кроссплатформенного приложения на C# с использованием библиотеки LXUI (написана на .NET 4.0 и позволяет использовать устаревшие среды разработки и маломощное железо).
  • Использование класса DbBatch в .NET 6: предпосылки и основные преимущества.
  • Реализация внедрения зависимостей в приложении ASP.NET Core на практике.
  • Подборка вопросов и ссылок на развёрнутые ответы для подготовки к собеседованию на позицию Unity-разработчика.
  • Хранение динамических пользовательских данных с использованием Entity Framework Core: обзор двух стратегий и их достоинств/недостатков.
  • Разбор пяти ключевых функций C# 11 с акцентом на проблемы в текущей версии языка.
  • Процесс проектирования и создания PDF-документов с использованием C# стал значительно проще благодаря open source библиотеке QuestPDF. Смотрите сами:
  • Руководство по использованию Scopes (одна из функций Serilog) и Seq для улучшения логирования в .NET 6.
  • Базовое руководство по работе с MySQL в ASP.NET Core для начинающих.
  • Волшебство, которое остается за кулисами ваших C#-программ.
  • Разбираем два способа использования Entity Framework Core + ASP.NET Core для создания мультитенантных приложений.
  • Онлайн-учебник с множеством советов и рекомендаций по работе с Azure.
  • Основные преимущества использования типа record при реализации шаблона Строитель в C#.
  • Более 70 000 разработчиков рассказали о том, как они учатся и повышают свой уровень, какие инструменты используют и чего хотят.
  • Подборка свежих руководств от Code Maze:

Ternary Operator ? : in C#

Remove Duplicates From a List in C#

Counting Sort in C#

How to Implement Retry Logic in C#

Method Overloading In C#

Pattern Matching in C#

Introduction to Regular Expressions in C#

Radix Sort in C#

Dictionary in C#

Console Class in C#

Global Using Directives in C#

Stack in C#

Bucket Sort in C#

Книги

Learn WinUI 3.0: Leverage the power of WinUI, the future of native Windows application development (2021), Автор: Alvin Ashcraft
Learn WinUI 3.0: Leverage the power of WinUI, the future of native Windows application development (2021), Автор: Alvin Ashcraft

Перед вами руководство по созданию приложений для современных версий Windows с использованием WinUI.

Книга охватывает следующие темы:

  • Работа с WinUI, Visual Studio и UWP
  • Разработка тестируемых и поддерживаемых приложений с использованием шаблона MVVM
  • Знакомство с мастером создания новых проектов Windows Template Studio и библиотеками WinUI
  • Модернизация приложения WPF и WinForms с помощью WinUI и XAML Islands
  • Использование системы Fluent Design для создания красивых приложений WinUI и многие другие

В общем, если вы знакомы с UWP/WPF, но хотите расширить свои знания в области Windows-разработки и модернизации существующих приложений, эта книга будет вам полезна. Предварительных знаний WinUI не требуются, но практический опыт работы с C# и .NET приветствуется.

C/C++

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека C/C++ разработчика»

Новости

Статьи и руководства

  • Используйте флаги компилятора для защиты стека в GCC и Clang.
  • Сопрограммы в C++: что это значит, когда я объявляю свою сопрограмму как noexcept?
  • 5 причин, по которым вам следует использовать C++ для цифровой обработки сигналов.
  • TIL о налоге на время компиляции std::ranges, который платят все, кто использует C++20.
  • Мое cобеседование c европейской компанией. Senior C++ Developer.

Библиотеки

  • FlatBuffers – библиотека сериализации с эффективным использованием памяти.
***

Предыдущие выпуски

13
Июн
2022

📰 Weekly #4: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C# и C/C++

В этом выпуске: беспрецедентный фотореализм нейросети Imagen; все, что вам нужно знать о звездочках в Python; обзор дизайнерских решений, которые формируют общий успех Go; рекомендации по повышению производительности в C#; почему C++20 – са…

31
Май
2022

📰 Weekly #2: новости, подкасты, отборные статьи и обучающие материалы по Python, Data Science, Go, C# и C/C++

Во втором выпуске: не идите за стадом, чтобы получить работу в DS; простой пример использования дженериков в Go; лямбда-выражения в C#; мнение Лёши Корепанова о карьерных возможностях Unity-разработчиков; подробный обзор шаблонов в С++ и многое другое.

Python

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»


***

Data Science

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека data scientist’а»

Новости

Руководства


  • Woodwork: пространство имен данных для многих инструментов машинного обучения.
  • Pandas Tutor: использование Pyodide для обучения науке о данных в масштабе.
  • Забудьте о Jupyter, демонстрируйте свои данные с помощью информационных панелей.
  • Эволюция генераторов случайных чисел.
  • Очистка данных с помощью Python с использованием библиотеки Pandas.
***

Go

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Новости

Статьи и руководства


Книги


Книга Go, from the beginning — Chris Noring об основах Go, которая охватывает следующие темы:

  • Создание CLI-приложений и веб-API.
  • Тестирование кода.
  • Разработка и публикация пакетов.
  • Организация кодовой базы.
  • Работа с файлами и каталогами.
  • Парсинг текста и многое другое.
***

C#

Новости

Статьи

GameDev и Unity

  • Unity постоянно работает над улучшением интеграции с .NET, включая новые функции C#. Узнайте, что будет дальше во взаимодействии Unity с экосистемой .NET. Читать
  • Карьерные возможности Unity-разработчиков.
  • Геймдевам на заметку: JetBrains представила RiderFlow, бесплатный плагин для редактора Unity, который позволяет управлять сложными сценами, легко перемещаться по сцене, находить нужные ресурсы и многое другое.

Книги

  • Ultimate Guide to Profiling Unity Games — руководство по профилированию в Unity, в котором собраны знания и советы внешних и штатных экспертов Unity: как профилировать приложение в Unity, управлять его памятью и оптимизировать энергопотребление:
Ultimate Guide to Profiling Unity Games
Ultimate Guide to Profiling Unity Games
***

C/C++

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека C/C++ разработчика»

Новости

Статьи и руководства

22
Май
2022

🏎 К чему приводит обилие конкурентности в кодовой базе Go?

В статье поговорим о том, какие проблемы возникают при конкурентном обращении нескольких потоков к одним и тем же данным.

Проблемы многопоточной разработки

В наше время существуют десятки крупных компаний, одновременно обслуживающих миллионы клиентов по всему миру и оказывающих им различные услуги. При этом, многие из них используют Go в качестве основного языка программирования для разработки своих микросервисов, архитектура которых порой работает на нескольких миллионах процессорных ядер. К примеру, монорепозиторий известного перевозчика Uber содержит около 46 миллионов строк кода и около 2100 уникальных сервисов. Использование языка Go для этих целей обусловлено рядом весомых преимуществ: грамотно настроенный параллелизм, простая настройка сборки мусора, минималистичный подход к синтаксису, отличные инструменты для дальнейшей обработки и обслуживания кода, поддержка библиотек, а также растущее с каждым днем, сообщество разработчиков.


В своей работе, Go-разработчики для передачи и обмена информацией используют легковесные потоки – горутины. Так вот, при параллельном обращении нескольких горутин к одним и тем же данным и начинают возникать большие проблемы, называемые в Go – состоянием гонки (Race conditions). А учитывая обилие кодовой базы и количество обрабатываемых одновременно процессов – проблема принимает глобальный масштаб. В статье попробуем разобраться, что это за напасть такая и как от нее избавиться.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»

Параллелизм и конкурентность

Прежде чем поговорить о гонках данных (data races), необходимо упомянуть о концепциях параллелизма и конкурентности, с которыми обязательно нужно разобраться, перед тем как заниматься многопоточной разработкой. Эти понятия схожи, но имеют существенные различия – «Concurrency is not Parallelism!». Суть этого расхожего выражения в том, что конкурентность – это методика проектирования вашей программы, параллелизм – это один из способов ее выполнения.

Говорить о конкурентности можно, когда работа приложения предполагается с несколькими одновременно запущенными задачами при создании процессов, выполняющихся независимо друг от друга. При этом, для достижения необходимого поведения, процессов в приложении может быть большое количество.

В одноядерных системах конкурентное выполнение потоков имеет модель, при которой контекст переключается между задачами, то есть программа может работать сразу с несколькими задачами, однако, не сможет выполнить их все вместе, ведь ядро всего одно. Причем переключается он в этом случае настолько быстро, что может создаться впечатление, что выполняемые процессы завершаются одновременно.

При этом говорить о параллелизме мы не можем, ведь задачи не могут выполняться вместе просто потому, что система у нас – одноядерная.

Проще говоря, когда мы имеем несколько одновременно выполняющихся потоков инструкций – надо делать так, чтобы код выполнялся параллельно, а этот процесс требует конкурентности. Ведь при отсутствии конкурентного дизайна распараллелить нашу программу не выйдет. А вот сама по себе концепция конкурентности не обязательно нуждается в параллелизме, ведь если программа способна работать на нескольких ядрах, она спокойно может функционировать и на одном.

Состояния гонки в Go


Проблемы, относящиеся к категории состояние гонки, довольно трудно обнаружить и поэтому они относятся к группе самых непредсказуемых ошибок программирования. Довольно часто при запуске кода они вызывают загадочные сбои, понять природу которых не может и матерый разработчик. При этом, даже упрощающий написание чистого кода механизм конкурентности, не сможет предотвратить Race conditions. В таких случаях, от программиста требуется осторожность, больше усердия и грамотное тестирование.

Гонка происходит, когда две или более горутины обращаются к одним и тем же данным. Сбои, вызванные гонками данных в программах Go, повторяются и зачастую снижают эффективность и производительность наиболее важных функций, что будет доставлять неудобства вашим клиентам и повлияет на их доход. Порой разработчикам довольно трудно отлаживать data races и иногда некоторые из них исправляют такие ошибки с помощью консервативных методов. Одним из которых является отключение параллелизма в подозрительных областях кода.

Для того, чтобы иметь возможность заранее диагностировать приложение для обнаружения такого рода ошибок разработчики языка создали функционал, называемый детектор гонки.

Детектор гонки

Впервые этот набор инструментов, определяющих состояния гонки в Go-коде, появился в версии 1.1 и по сей день, успешно работает на операционках Linux, OS X и Windows с 64-разрядными процессорами x86.

В основе детектора лежит библиотека времени выполнения (runtime library) C/C++ – ThreadSanitizer, используемая для обнаружения ошибок в кодовой экосистеме Google и Chromium. Внедрили эту технологию в Go в сентябре 2012, и после этого она стала частью непрерывного процесса сборки по отслеживанию условий гонки при их возникновении.

Race detector встроен в цепочку инструментов Go и работает следующим образом:

  • В необходимых нам подозрительных местах мы устанавливаем флаг командной строки -race, показывающий компилятору полный список обращений к памяти с помощью кода, описывающего основные параметры осуществления доступ к памяти.
  • А тем временем, ThreadSanitizer ищет в коде несинхронизированные обращения к общим переменным и, обнаружив такое «грубое» поведение, выдает предупреждение.

Детектор сконструирован таким образом, что он в состоянии обнаружить Race conditions только при фактическом запуске кода. Поэтому важно осуществлять запуск двоичных файлов приложения при реалистичных рабочих нагрузках.

Но двоичный файл с детектором гонки может сильно влияет на производительность процессора и памяти, вызывая перегрузку системы – поэтому не стоит держать детектор гонки включенным постоянно. Хорошей практикой является запуск его, вместе с нагрузочными и интеграционными тестами, ведь они, в большинстве своем, используют конкурентные части кода.

Как использовать детектор гонки

Как мы уже упоминали ранее, для того, чтобы подключить к необходимому участку кода включенный детектор гонки, к нему нужно добавить флаг -race, например:

        $ go test -race pack // тестируем пакет
$ go run -race src.go // компилируем и запускаем программу
$ go build -race cmd // сборка команды
$ go install -race pack // устанавливаем пакет
    

Рассмотрим пример упрощенной версии фактической ошибки, которую обнаружил детектор гонки. Здесь он с помощью таймера выводит сообщения с произвольным интервалом от 0 до 1 секунды. Все это действие повторяется пять секунд.

Он использует time.AfterFunc для создания Timer для первого сообщения, а затем использует метод Reset для планирования следующего сообщения, каждый раз повторно используя Timer.

        func main() {
start := time.Now()
var t *time.Timer
 t = time.AfterFunc(randomDuration(), func() {
 fmt.Println(time.Now().Sub(start))
 t.Reset(randomDuration())
 })
time.Sleep(5 * time.Second)
}
func randomDuration() time.Duration {
   return time.Duration(rand.Int63n(1e9))
}

    
***

Race detector – мощнейший инструмент, проверяющий корректность всех параллельных программ, к предупреждениям которого нужно отнестись со всей серьезностью, ведь ложных срабатываний он не выдает. Обратите внимание, что детектор находит лишь те гонки, которые могут появиться в процессе выполнения приложения, поэтому он не может найти их на участках кода, которые не выполняются.

Материалы по теме

30
Дек
2021

👨‍🎓️ ТОП-11 бесплатных учебных курсов по Go

Библиотека программиста сделала для вас подборку актуальных бесплатных учебных курсов по Golang.

В конце 2000-х корпорация Google, почувствовав потерю производительности и замедление рабочего процесса из-за большой и сложной кодовой базы, разработала свой язык программирования, ориентированный на скорость и простоту. Созданный для решения внутренних проблем компании, он быстро набрал популярность и стал одним из ведущих современных языков. Golang стали использовать повсеместно для программ, связанных с сетями и инфраструктурой, в качестве высокопроизводительного серверного языка. На сегодняшний день он входит в 10-ку самых популярных языков программирования согласно опросу Stack Overflow. Это ли не повод начать его изучать?

В помощь всем желающим освоить профессию go-разработчика, библиотека программиста сделала подборку актуальных бесплатных учебных курсов по Golang на русском языке.

1. Программирование на Golang от Stepik


На этом курсе, посвященном основам языка Golang, вы изучите базовые возможности языка: циклы, срезы, горутины, работа с JSON и многое другое. Множество практических задач разного уровня для тренировки и закрепления пройденного материала, проверяемых автоматической системой для быстрой обратной связи. Преподаватели курса всегда отвечают на возникшие вопросы в комментариях. Там же вы можете общаться с однокурсниками на интересующие вас темы.

Программа курса:

  1. Введение в Go.
  2. Первая программа.
  3. Типы данных, переменные, константы.
  4. Комментарии.
  5. Условные выражения и конструкции.
  6. Циклы, функции, структуры, указатели.
  7. Массивы и срезы.
  8. Файлы, интерфейсы, многопоточность.

Курс предназначен для слушателей, знающих основы хотя бы одного из языков программирования.

По окончании предоставляется сертификат.

Длительность: 30 уроков.

2. Курс Learn Go от Be Geek


Серия видеоуроков по основам программирования на Go от Youtube-канала Be Geek с разбором всех основных тем и практических примеров, необходимых для быстрого старта.

Программа курса:

  1. Типы и переменные в Golang.
  2. Циклы, массивы и срезы.
  3. Функции, структуры, организация кода.
  4. Библиотека Sort.
  5. Парсер на Go.
  6. Telegram и Go. Разработка телеграм-бота.
  7. Go и Docker.

Также в программе есть несколько роликов о заработке на фрилансе. Теоретические лекции без тестов и практических заданий. Подходит для новичков.

Длительность: 24 урока.

3. «Программирование на Go» от VK Team


Девять двухчасовых видеолекций по основам Golang-разработки от программистов из VK Team дадут вам начальный багаж знаний для дальнейшего развития в профессии.

Программа курса:

  • Введение.
  • Функции, структуры, интерфейсы. Объектная модель.
  • Асинхронная модель.
  • Web. Работа с сетью.
  • Работа СУБД.
  • Система тестирования.
  • Reflect или Generate?
  • Производительность.
  • Context, unsafe, safe.

Подойдут для быстрого старта всем желающим изучить язык Go.

Длительность: 9 уроков.

Больше полезной информации вы можете получить на нашем телеграм-канале «Библиотека Go разработчика».

4. «Golang для начинающих» от itProger


Небольшой курс по Go, рассчитанный на новичков. Прослушав его, вы научитесь кодить на базовом уровне, изучите основные конструкции языка и создадите на Go небольшой веб-сайт с поддержкой баз данных MySQL.

Программа курса:

  • Введение в язык Go.
  • Отслеживание URL-адресов в языке Go.
  • Создание структур (модели данных).
  • Работа с HTML шаблонами в Go.
  • Подключение MySQL к Golang проекту.
  • Новостной сайт. Главные настройки.
  • Добавление данных через сайт.
  • Динамические страницы для отображения статей.
  • Публикация Golang-проекта на сервер.

Длительность: 9 уроков.

5. «Разработка веб-сервисов на Go – основы языка» от МФТИ совместно с Mail.ru Group


Курс по основам программирования на Go с примерами основных задач, часто встречающихся в современной серверной веб-разработке. Предназначен для слушателей с базовыми знаниями программирования. В лекциях используется профессиональный жаргон без подробных пояснений. Не подходит для абсолютных новичков.

Программа курса:

  • Введение в Golang.
  • Асинхронность и конкурентность.
  • Работа с динамическими данными и производительность. Json.
  • Основы HTTP.

После прохождения курса вы получите электронный сертификат.

Длительность: 1 месяц с момента начала занятий.

6. «Разработка веб-сервисов на Golang» от МФТИ совместно с Mail.ru Group (2 часть)


Вторая часть предыдущего курса расскажет об анатомии веб-сервисов. Вы узнаете этапы разработки, а также научитесь работе с хранилищами и микросервисами.

Программа курса:

  • Анатомия веб-сервиса.
  • SQL и NoSQL.
  • Микросервисы. Фреймворк gRPC.
  • Конфигурирование и мониторинг сервисов. Интеграция кода на С в GO.

Длительность: 4 недели с момента начала занятий.

7. «Курс по изучению Golang для начинающих» от golangify.com


Полный курс по изучению основ программирования на Golang для начинающих с примерами кода и полным описанием рассмотренных паттернов.

Программа курса:

  • Введение в Go: пакеты, функции, работа с числами, форматирование строк, переменные и константы.
  • Циклы for, if-else-switch, True-False и операторы сравнения.
  • Область видимости переменных, типы данных и работа со строками.
  • Работа с массивами и срезами в Golang – append() и make().
  • Функции в Golang на примерах.
  • Структуры и методы – объектно-ориентированный подход в Golang.
  • Горутины и конкурентность.

Практика:

  • Создание программы для покупки билетов.
  • Создание игры «Жизнь».
  • Программа для перевода температуры из Цельсия в Фаренгейты.
  • Создание игры Судоку.
  • Создание игры-симулятора фермы.

Курс похож прикладной учебник – удобный текстовый справочник со всей необходимой теорией и практическими примерами.

Подходит для новичков (но требует знания основ программирования)

Длительность: 35 уроков.

8. «Основы Golang» от ITVDN


Очередная серия обучающих видеороликов по теме от ребят из команды ITVDN. Просмотрев его, вы научитесь создавать простые приложения на Go, работать со стандартными библиотеками и писать многопоточные приложения

Программа обучения:

  • Основные понятия, стиль написания, пакеты и типы.
  • Первая программа на языке Go, утилиты Go.
  • Массивы, срезы, словари, оператор range.
  • Структуры и интерфейсы. Интерфейсный тип данных.
  • Многопоточность. Горутины, каналы, оператор select.
  • Пакеты. GoDoc.
  • IO, пакет bytes, буферы.
  • Тестирование. Измерение производительности функций.

Подходит для абсолютных новичков.

Длительность: 8 уроков.

9. «Научись программировать на Go» от Булата Замалутдинова


Несмотря на то, что курс небольшой, у него достаточно интересная и обширная программа. В видеоуроках предоставлена теория программирования на языке Golang и даны понятные пояснения.

Программа курса:

  • Переменные и функции.
  • Массивы, циклы.
  • Инкапсуляция, типы.
  • Константы, работа со строками, первая обработка ошибок.
  • ООП, инкапсуляция.
  • Полиморфизм, стандартная библиотека.
  • Горутины, синхронизация потоков, мьютексы.
  • Каналы, конструкция select.
  • Web-программирование, парсинг json, использование библиотек.
  • Работа с json.

Подходит для зрителей без опыта программирования

Длительность: 10 уроков.

10. «Погружение в Google Go» от Романа Левищенко


Курс по основам программирования на Go, затрагивающий важные вопросы разработки, c представленными исходниками и материалами для самостоятельного обучения.

Программа обучения:

  • Введение.
  • Переменные, константы и типы данных.
  • Инструкции, функции, области видимости.
  • Каналы и тестирование.

Только базовая информация о языке и программировании, нет практических задач и обратной связи.

Подходит для новичков.

Длительность: 25 уроков.

11. «Уроки Golang» от AJ Golang Learn


Курс дает базовые знания разработки на Go и будет понятен даже абсолютным новичкам в программировании. Состоит из небольших видеолекций, изложенных простым понятным языком.

Программа обучения:

  • Настройка рабочего окружения Windows.
  • Организация кода (пакеты).
  • Управляющие конструкции.
  • Массивы, функции, замыкания.
  • Интерфейсы.

Подходит для людей без базового знания программирования

Длительность: 18 уроков.

Курсы на английском

Добавим несколько курсов по теме на английском языке, пользующихся популярностью у иностранных слушателей:

Важно помнить, что в трудных ситуациях вы всегда можете воспользоваться – спецификацией языка. А вот подборка официальных ответов разработчика языка на самые часто задаваемые вопросы пользователей о программировании на Go.

***

Как гласит китайская мудрость: «Не бойся, что не знаешь – бойся, что не учишься!» Это к тому, что при должном стремлении и упорстве вы всегда добьетесь поставленной цели.

Удачи в обучении!

Материалы по теме

24
Ноя
2021

🎥 10 лучших каналов по Go (Golang) на YouTube для новичков и профессионалов

Языки программирования растут как грибы после дождя. Один из них – Go (Golang) и его талисман Gopher. Предлагаем узнать, что эти двое могут предложить разработчикам. Читайте наш обзор лучших тематических каналов на YouTube.

07
Сен
2021

🏃 Пишем мессенджер на Go за час: 7 простых шагов от эхо-сервера к асинхронному обмену сообщениями

Авторы большинства статей по сокетным соединениям в примерах ограничиваются реализацией эхо-сервера. Давайте разовьем эту тему и за 7 простых шагов сделаем вместе консольный мессенджер сообщений.

Шаг 1. Ищем код эхо-сервера

Типовой эхо-сервер, на который дал мне первую ссылку Yandex, описан в статье «Golang: простой сервер TCP и TCP-клиент». Если вы не имеете представления, как работают сокеты и соединения, стоит её почитать. Исходный код из этой статьи можно скачать тут и желательно сразу же его запустить.

Если скомпилировать исходники и запустить в отдельных консолях клиент и сервер, то можно обмениваться сообщениями. У этого сервера есть один значительный недостаток: он работает только с одним клиентом. Попробуем запустить в еще одной консоли новый клиент и увидим, как она зависнет. То же самое будет и с четвертым, и с пятым клиентом.

Шаг 2. Реализуем прием нескольких соединений

Чтобы принять несколько одновременных соединений, необходимо:

  • функцию приема соединения conn.Accept() заключить еще в один цикл for.
  • весь код, который был в цикле, вынести в отдельную функцию process().
  • запустить функцию process() как отдельную горутину в цикле for сразу после приема соединения conn.Accept()
Подробнее о горутинах и каналах рассказывается в статье «Параллельное программирование в Go». Стоит с ней ознакомиться, поскольку на этих механизмах основывается наш будущий проект.

В результате небольших изменений наш код примет следующий вид:

           // функция process запускается как горутина
    func process(conn net.Conn){
      // определим, что перед выходом из функции, мы закроем соединение
      defer conn.Close
      for {
         // Будем прослушивать все сообщения разделенные \n
         message, _ := bufio.NewReader(conn).ReadString('\n')
         // Распечатываем полученое сообщение
         fmt.Print("Message Received:", string(message))
         // Отправить новую строку обратно клиенту
         conn.Write([]byte(message + "\n")){
        }
    } 
    

Код в main:

             // Устанавливаем прослушивание порта
    ln, _ := net.Listen("tcp", ":8081")
    // выполнение цикла обработки соединений
    for {    
      // Принимаем входящее соединение
      conn, _ := ln.Accept()
      // запускаем функцию process(conn)   как горутину
      go process(conn)
    }
    
Внешний цикл (строки 31-36, код доступен в репозитории) будет принимать входящее соединение, а внутренний цикл (строки 15-19) будет обрабатывать входные данные. Исходный код клиента мы не меняем.

Если мы запустим в одной консоли сервер, а в нескольких других консолях программу клиента, то можно видеть, что сервер может обрабатывать уже несколько соединений с клиентами параллельно. На самом деле он их обрабатывает асинхронно: сперва один запрос, потом другой, осуществляя переключения между горутинами.

Шаг 3. Обрабатываем ошибки соединений

Давайте попробуем отсоединить один из клиентов, убив его процесс: наш сервер зациклится. Если что-нибудь набрать в клиенте, то данные куда-то уходят, и клиент ни о чём не подозревает.

Мы забыли обработать ошибки ввода-вывода. Функция вывода в сокет Write имеет два выходных параметра: кол-во считанных байт и ошибку:

        data_len, err := conn.Write(b []byte)
    

Если ошибка не пустая (т.е. не равна nil ), значит мы не смогли принять данные. Какая ошибка произошла, можно узнать с помощью функции err.Error()

Заменим conn.Write(b []byte) на следующий код:

        _ , err := conn.Write(b []byte)
if err != nil {
          fmt.Println(err.Error)
          break  // выходим из цикла и функции
} 
    

Аналогичный код пропишем в клиенте. Еще в клиенте отсутствует отложенное закрытие соединения, которое срабатывает при выходе из функции defer conn.Close().

Если вы не смогли внести изменения самостоятельно, готовый код можно подглядеть на GitHub.

Теперь при закрытие клиента или сервера, у нас будет выдаваться сообщение:

        write tcp 127.0.0.1:8081->127.0.0.1:40296: write: broken pipe
    

Шаг 4. Простой прототип мессенджера

К этому моменту мы допилили эхо-сервер, а теперь осталось сделать простой из него мессенджер. Пусть логика мессенджера будет следующей:

  • Клиент коннектится к серверу и получает номер сообщения. Каждай клиент имеет свой уникальный номер.
  • Клиент отправляет сообщение серверу с указанием номера клиента, кому адресовано это сообщение.
  • Сервер принимает сообщение от клиента, декодирует его и отправляет тому клиенту, которому адресовано это сообщение.
Нам осталось усовершенствовать сервер так, чтобы определить, с какого клиента было отправлено сообщение. Для простоты пусть каждый клиент будет иметь номер, соответствующий порядковому номеру соединения начиная с нуля (индексы массивов в Go начинаются с нуля).

Введем счетчик входящих соединений, а каждое новое соединение сохраним в xeштаблице, организовав таким образом пул:

        conns := make( map[int] net.Conn, 1024)
    

Каждое соединение после conn.Accept() мы сохраним в conns, а в функцию process() будем передавать весь пул (хештаблицу) и номер текущего соединения. В функции обработки соединения process() мы можем иметь доступ ко всем активным соединениям. Не забываем увеличивать на единицу счетчик текущих соединений.

В функции process() мы принимаем не текущее соединение, а пул и номер текущего соединения. Следовательно, чтоб получить доступ к текущему соединению, мы можем его взять из пула:

        conn := conn[n]
    

Новый код сервера:

        func process(conns map[int]net.Conn, n int) {
  // получаем доступ к текущему соединению
  conn := conns[n]
  // определим, что перед выходом из функции, мы закроем соединение
  defer conn.Close()
  for {
    // Будем прослушивать все сообщения разделенные \n
    message, _ := bufio.NewReader(conn).ReadString('\n')
    // Распечатываем полученое сообщение
    fmt.Print("Message Received:", string(message))
    // Отправить новую строку обратно клиенту
    _, err := conn.Write([]byte(strconv.Itoa(n) + "->> " + message + "\n"))
    // анализируем на ошибку
    if err != nil {
      fmt.Print(err.Error())
      break // выходим из цикла
    }
  }
}
func main() {
  fmt.Println("Start server...")
  // создаем пул соединений
  conns := make(map[int]net.Conn, 1024)
  i := 0
  // Устанавливаем прослушивание порта
  ln, _ := net.Listen("tcp", ":8081")
  // объвляем пул соединений на 1024 соединения
  conns := make(map[int]net.Conn, 1024)
  // Запускаем цикл обработки соединений
  for {
    // Принимаем входящее соединение
    conn, _ := ln.Accept()
    // сохраняем соединение в пул
    conns[i] = conn
    // запускаем функцию process(conn)   как горутину
    go process(conns, i)
    i++
  }
}
    
Полный код примера можно найти в этом репозитории. Попробуем запустить сервер и отправить сообщение.

При тестировании мы видим, что в каждом ответном сообщении сервер возвращает клиенту номер текущего соединения:

        ./client
Text to send: msg from 1
Message from server: 1->> msg from 1
    

Шаг 5. Реализация протокола обмена

Остается малость: определить как, кто и кому будет отправлять сообщения. Создадим простейший протокол обмена, в котором первые байты сообщения будут содержать номер клиента и текст через пробел.

Пример:

        2 Оправляем сообщение второму клиенту
    

Для реализации этого протокола, необходимо сделать парсинг сообщения. Номер клиента, мы вытащим, используя fmt.Scanf(), а само сообщение с использованием слайса:

        // парсинг полученного сообщения
fmt.Sscanf(message, "%d", &clientNo) // определи номер клиента
pos := strings.Index(message, " ") // нашли позицию разделителя
out_message := message[pos:] // взяли хвост сообщение после пробела

    

Дальше все очень просто: зная номер соединения (clientNo) клиента, мы будем отправлять ответ в нужное соединение. Сообщение было немного изменено, и теперь мы выводим, от какого клиента оно исходит:

        conns[clientNo].Write([]byte(strconv.Itoa(clientNo) + "->> " + out_message + "\n"))
    
Запускаем, проверяем и видим некоторые баги. Если мы направляем сообщение самому себе, то вроде бы все работает. Если отправить сообщение другому клиенту, оно где-то теряется, а если этим клиентом отправить сообщение кому-то еще, оно появляется из ниоткуда. Что же пошло не так?

Шаг 6. Распараллеливание кода

Наш клиент работает синхронно: после отправления сообщения он переходит в режим ожидания чтения сокета. Все работает согласно алгоритму: записал и жди ответа. Если мы отправим сообщение самому себе, то примем его без проблем. А если отправим другому клиенту, то после этого встаем в режим ожиданий. Второй клиент тоже стоит в режиме ожидания и примет наше сообщение, только после того, как сам что-нибудь отправит. Как же выйти из этой ситуации?

Выход довольно простой: нужно чтение из сокета и чтение с консоли сделать асинхронными, т.е. сделать так, чтобы ввод из сокета и ввод из консоли не блокировали друг друга. Как говорилось выше, горутины позволяют коду выполняться асинхронно. Следовательно, должны быть запущены две разные горутины: одна должна осуществлять чтение с консоли, а вторая – чтение из сокета.

Первая горутина читает данные из сокета, и если в сокете есть данные, выводит их на печать. Чтение и вывод должны быть заключены в цикл:

        // прием данных из сокета и вывод на печать
func readSock(conn net.Conn ) {
    buf := make([]byte,256)
    for {
        readed_len, _ := conn.Read(buf)
        if readed_len > 0 {            
            fmt.Print(string(buf))
        }
    }
}

    

Со второй горутиной все немного сложнее, поскольку она должна передать данные в основную программу. Почему нельзя сразу писать их в сокет, как это делает первая горутина? Увы, операция conn.Write() – блокирующая, и если мы так сделаем, то можем заблокировать другие операции ввода-вывода. Все блокирующие операции нужно разнести по разным асинхронным частям программы.

        // ввод данных с консоли и вывод их в канал
func readConsole(ch chan string) {
    for {
        fmt.Print(">")
        line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
        out :=line[:len(line)-1] // убираем символ возврата каретки
        ch <- out // отправляем данные в канал
    }
}

    

Основная программа должна запустить две асинхронных горутины: чтение с консоли и из сокета (в цикле читать канал и если в нем есть данные, то записать их в сокет). Чтобы наше консольное приложение не «съело» все ресурсы CPU, необходимо ввести некоторую задержку: time.Sleep(time.Seconds * 2)

Должно получиться примерно следующее:

        func main(){
    ch := make(chan string)

    defer close(ch) // закрываем канал при выходе из программы

    conn, _ := net.Dial("tcp", "127.0.0.1:8080")

    go readConsole(ch)
    go readSock(conn)

    for {
        val, ok := <- ch
        if ok { // если есть данные, то их пишем в сокет

            _, err := conn .Write([]bytes(val))
            if err != nil {
                fmt.Println("Write:", err.Error())
                break
            }
        } else {
            // данных в канале нет, задержка на 2 секунды
            time.Sleep(time.Second * 2)
        }

    }
    fmt.Println("Finished...")
    conn.Close()
}

    

Шаг 7. Повышаем надежность выполнения кода

Наше приложение должно работать при любых входных данных, даже если они некорректные. Есть несколько простых правил, которые придется соблюдать при построении любых приложений:

  • необходимо проверять все входящие данные и если их длинна больше принимающего буфера, то либо обрезать их, либо генерировать ошибку;
  • необходимо обрабатывать все функции ввода-вывода на возможность возникновения ошибки.

Например, в коде было много сокращений и специально была опущена обработка функций conn.Accept() и net.Dial():

            ls,err := conn.Accept()
    if err != nil {
        fmt.Println(err)
        panic("accept")
    }

    

Также был опущен код обработки объема данных с консоли:

            // ввод данных с консоли
    for {
        fmt.Print(">")
        line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
        if len(line) > 250 {
            fmt.Println("Error: message is very lagre")
            continue
        }
        ch <- b
    }    
    

Почти готовое и работоспособное решение можно найти в репозитории: вам остается самостоятельно дописать обработку всех ошибок ввода-вывода. Если возникнет желание сделать pull request в репозиторий, то я смогу указать в комментариях на ошибки или просто похвалить. Сделайте свой код достоянием общественности. Удачи!

20
Июл
2021

🏃 43 ресурса для изучения Golang в 2021 году: сайты, блоги, сообщества, курсы, каналы и книги

Язык Go не самый распространенный, но, определенно, один из наиболее востребованных у работодателей. Читайте актуальную подборку ресурсов для его изучения: в ней есть все необходимое, включая свежие книги на английском языке.

18
Июл
2021

🛠 Взаимодействие MySQL и Go: подводные камни автоматической кодогенерации

Большинство статей про использование MySQL в Golang повторяет примеры из официального руководства. Реальная разработка далека от простых примеров: из-за строгой типизации часто возникают проблемы. Разбираемся с их решением, если вам необход…

18
Июл
2021

🛠 Взаимодействие MySQL и Go: подводные камни автоматической кодогенерации

Большинство статей про использование MySQL в Golang повторяет примеры из официального руководства. Реальная разработка далека от простых примеров: из-за строгой типизации часто возникают проблемы. Разбираемся с их решением, если вам необход…

12
Июл
2021

🏃 Самоучитель для начинающих: как освоить Go с нуля за 30 минут?

Важнейшей ценностью для создателей Go была простота. Код на этом языке легко писать, легко читать и в результате – проще поддерживать. В кратком руководстве мы окунемся в основы Go за полчаса.

03
Июн
2021

🐛 Исключения в Go – это легко?

В Go (Golang) нет специального механизма обработки исключений, и создатели языка не собираются его добавлять. Попробуем разобраться, хорошо это или плохо и как лучше разрешать проблемные ситуации в приложениях.

23
Май
2021

🏃 Параллельное программирование в Go

Изучаем основы параллельного программирования в Go, а также пытаемся разобраться на примерах, почему конкурентность в Go – это не совсем параллелизм.

Перевод публикуется с сокращен…

20
Апр
2021

🕸 Golang для веб-разработки: примеры использования

Разбираем преимущества и недостатки языка Go для веб-разработки и рассказываем о написанных на нем (и для него) приложениях, библиотеках и фреймворках, которые можно использовать в собственных проектах.

Go особенности и немного истории

Go (Golang) представляет собой компилируемый, многопоточный, статически типизированный язык. Он был разработан командой Google в 2007 году для улучшения производительности высоконагруженных серверных приложений.

Первоначально к языку предъявлялись следующие требования:

  • Он должен работать в большом масштабе для программ с большим количеством зависимостей.
  • Он должен быть понятен знакомым с C программистам. Многие разработчики Google хорошо владели процедурными языками из семейства C. Необходимость быстро обучить специалистов требовала, чтобы Go не имел радикальных отличий от них.
  • Он должен быть современным. C, C++ и в некоторой степени Java – довольно старые. Они были разработаны до появления многоядерных машин, сетей и веб-приложений. Особенности современного мира требуют внедрять новые подходы вроде встроенного параллелизма.

Go – отличный вариант для создания простых, но эффективных веб-сервисов. Разберем вкратце основные плюсы и минусы этого языка для веб-разработки.


Преимущества

  • Высокая скорость. Go быстро компилирует проекты и может применяться даже для редактирования кода прямо в интернете.
  • Совместимость с C. Это позволяет использовать многочисленные библиотеки, созданные для одного из самых распространенных языков.
  • Широкие возможности из коробки. Расширенная стандартная библиотека охватывает множество областей. В Go есть полностью рабочий веб-сервер и многое другое.
  • Документация как стандартная функция. Программисты могут легко документировать код и генерировать удобочитаемые данные прямо из комментариев.
  • Сборка мусора (автоматическое управление памятью) – ключевая функция языка Go. Он дает контроль над распределением памяти и упрощает оптимизацию приложений.
  • Масштабируемость и параллелизм. По мере развития приложений и сервисов приходится одновременно решать множество мелких задач, вроде доставки HTML в отдельные веб-браузеры. Go имеет множество встроенных функций, связанных с параллелизмом: в первую очередь goroutines и каналы. Goroutines – функции, которые выполняются одновременно с другими. Каналы позволяют различным goroutines взаимодействовать друг с другом.

Подробнее о возможностях Golang вы можете узнать из этой статьи.

Недостатки

  • Отсутствие параметрического полиморфизма для универсального программирования приводит к дублированию кода или небезопасным преобразованиям типов.
  • Нулевой интерфейс не равен интерфейсу нулевого объекта, что в сочетании с отсутствием алгебраических типов приводит к трудностям при обработке сбоев и базовых случаев.
  • Go не допускает появления открывающей скобки на отдельной строке, что заставляет программистов использовать один и тот же стиль скобок.
  • Семантика файлов в стандартной библиотеке Go в значительной степени основана на семантике POSIX и они плохо отображаются на платформе Windows.
Рекомендуем также ознакомиться самыми распространенными подводными камнями Go, а также сравнением Python и Golang по различным характеристикам.

Примеры использования Go

Go применяется в веб-разработке для решения разных задач. Мы собрали примеры самых интересных приложений, библиотек и фреймворков, чтобы поближе познакомить вас с возможностями языка.

Аутентификация

  • Authboss – модульная система, которая позволяет добавить в приложение функции аутентификации и авторизации.
  • Casbin – библиотека авторизации, поддерживающая модели контроля доступа, включая ACL, RBAC и ABAC.
  • Go-email-normalizer – библиотека Go для обеспечения канонического представления адреса электронной почты.
  • Go-Guardian – эта библиотека обеспечивает простой, понятный и идиоматический способ создания современного API и веб-аутентификации. Она поддерживает LDAP, базовую аутентификацию, токен-носитель и аутентификацию на основе сертификатов.
  • Go-oauth2-server – автономный сервер OAuth2, написанный на Golang и соответствующий спецификациям.
  • Permissions2 – библиотека для отслеживания пользователей, состояний входа и разрешений. Использует безопасные файлы cookie и bcrypt.

Боты

  • Ephemeral-roles – бот Discord для управления эфемерными ролями на основе присутствия участника голосового канала.
  • Go-chat-bot – бот для IRC, Slack и Telegram, написанный на Go.
  • Go-tgbot – оболочка API-интерфейса Pure Golang Telegram Bot, созданная из файла swagger, сеансового маршрутизатора и промежуточного программного обеспечения.
  • Hanu – фреймворк для написания ботов Slack.
  • Slack-bot – готовый к использованию Slack Bot для ленивых разработчиков: пользовательские команды, Jenkins, Jira, Bitbucket, Github.
  • Tenyks – сервис-ориентированный IRC-бот, использующий Redis и JSON для обмена сообщениями.

Базы данных

  • Bbolt – встроенная база ключей/значений для Go.
  • Bcache – согласованная библиотека Go для распределенного кэша в памяти.
  • Bitcask – это встраиваемая, постоянная и быстрая база данных типа ключ-значение (KV), написанная на чистом Go. Отличается предсказуемой производительностью чтения/записи, низкой задержкой и высокой пропускной способностью благодаря расположению битовой маски на диске (LSM + WAL).
  • CovenantSQL – база данных SQL на блокчейне.
  • Databunker – служба хранения личной информации, созданная в соответствии с GDPR и CCPA.
  • Hare – простая система управления базой данных, в которой каждая таблица хранится в виде текстового файла JSON с разделителями-строками.
  • Ledisdb – высокопроизводительный NoSQL, подобный Redis и основанный на LevelDB.
  • Rqlite – легкая распределенная реляционная база данных, построенная на SQLite.
  • Go-mysql – набор инструментов Go для обработки протокола MySQL и репликации.
  • Kingshard – высокопроизводительный прокси для MySQL на базе Golang.
  • Pg_timetable – расширенное планирование для PostgreSQL.

DNS

  • DNS – библиотека Go для работы с DNS.
  • Go-powerdns – привязки API PowerDNS для Golang.
  • Mdns – простая клиент-серверная библиотека mDNS (Multicast DNS) для Golang.
  • DDNS – персональный клиент DDNS с DNS Digital Ocean Networking в качестве серверной части.
  • Dyndns – процесс Background Go для регулярной автоматической проверки IP-адреса и внесения обновлений в одну или несколько динамических записей DNS для доменов Google при каждом изменении.
  • GoDNS – написанный на Go клиентский инструмент динамического DNS, поддерживающий DNSPod и HE.net.

Почта

  • Chasquid – SMTP-сервер, написанный на Go.
  • Email – надежная и гибкая библиотека электронной почты для Go.
  • Go-dkim – библиотека DKIM для подписания и проверки электронной почты.
  • Go-imap – библиотека IMAP для клиентов и серверов.
  • Go-simple-mail – очень простой пакет для отправки электронных писем с SMTP Keep Alive и двумя таймаутами: Connect и Send.
  • Hectane – легкий SMTP-клиент, предоставляющий HTTP API.
  • MailHog – тестирование электронной почты и SMTP с веб-интерфейсом и интерфейсом API.

Формы

  • Binding – связывает форму и данные JSON из запроса net/http со структурой.
  • Form – декодирует url.Values ​​в значения Go и кодирует значения Go в url.Values. Поддержка Dual Array и Full map.
  • Forms – не зависящая от платформы библиотека для анализа и проверки данных форм, которая поддерживает составные формы и файлы.
  • Qs – модуль Go для кодирования структур в параметры URL.

Веб GUI

  • App – пакет для создания приложений с GO, HTML и CSS. Поддерживает MacOS (поддержка Windows в процессе разработки).
  • Go-sciter – встраиваемый движок HTML/CSS/скриптов для разработки пользовательского интерфейса современного рабочего стола.
  • Webview – кроссплатформенное окно веб-просмотра с простыми двусторонними привязками JavaScript (Windows/macOS/Linux).

Обмен сообщениями

  • Asynq – простая, надежная и эффективная распределенная очередь задач для Go, построенная на основе Redis.
  • Beaver – сервер обмена сообщениями в реальном времени для создания масштабируемых уведомлений в приложениях, многопользовательских игр, приложений чата в браузере и программах для мобильных устройств.
  • Confluent-kafka-go – клиент Golang от Confluent для Apache Kafka и платформы Confluent.
  • Drone-line – отправка уведомлений Line с использованием двоичного кода, докера или Drone CI.
  • Glue – надежная библиотека сокетов Go и Javascript.
  • Gorush – сервер push-уведомлений с использованием APNs2 и google GCM.
  • Jazz – простой уровень абстракции RabbitMQ для администрирования очередей, публикации и использования сообщений.

Сети

  • DHCP6 – реализует сервер DHCPv6, как описано в RFC 3315.
  • Fortio – библиотека нагрузочного тестирования и инструмент командной строки, расширенный эхо-сервер и веб-интерфейс. Позволяет указать заданную посекундную нагрузку и записать гистограммы задержки, а также другую полезную статистику и построить график. Поддерживает TCP, HTTP и gRPC.
  • Ftpserverlib – полнофункциональная библиотека FTP-сервера.
  • Gmqtt – гибкая, высокопроизводительная библиотека брокера MQTT, которая полностью реализует протокол MQTT V3.1.1.
  • Go-stun – реализация клиента STUN (RFC 3489 и RFC 5389).
  • Goshark – использует tshark для декодирования IP-пакета и создания структуры данных для его анализа.
  • Gosnmp – собственная библиотека Go для работы с SNMP.

HTTP-клиенты

  • Heimdall – усовершенствованный HTTP-клиент с возможностями повтора и hystrix.
  • Resty – простой HTTP- и REST-клиент для Go, вдохновленный Ruby rest-client.
  • Sling – клиентская библиотека Go HTTP для создания и отправки запросов API.

Серверные приложения

  • Algernon – веб-сервер HTTP/2 со встроенной поддержкой Lua, Markdown, GCSS и Amber.
  • Fider – открытая платформа для сбора и систематизации отзывов клиентов.
  • Jackal – XMPP-сервер, написанный на Go.
  • RoadRunner – высокопроизводительный сервер приложений PHP, балансировщик нагрузки и менеджер процессов.

Тестирование

  • Failpoint – реализация точек отказа для Golang.
  • Rod – драйвер Devtools для упрощения веб-автоматизации и парсинга.
  • Testsql – генерирует тестовые данные из файлов SQL перед тестированием и очищает их после завершения.
  • Testfixtures – помощник для Rails-подобных тестовых инструментов, предназначенных для тестирования приложений баз данных.
  • Restit – микро-фреймворк для написания интеграционного теста RESTful API.
  • Assert – базовая библиотека утверждений, используемая вместе с собственным тестированием Go.

Сторонние API

  • Airtable – клиентская библиотека Go для API Airtable.
  • Anaconda – клиентская библиотека Go для API Twitter 1.1.
  • Aws-sdk-go – официальный SDK AWS для языка программирования Go.
  • Discordgo – привязки Go для Discord Chat API.
  • Ethrpc – привязки Go для Ethereum JSON RPC API.
  • Gami – библиотека Go для интерфейса Asterisk Manager.
  • Geo-golang – библиотека Go для доступа к Google Maps, MapQuest, Nominatim, OpenCage, Bing, Mapbox и API геокодирования, а также обратного геокодирования OpenStreetMap.
  • Github – библиотека Go для доступа к GitHub REST API v3.
  • Medium – SDK Golang для API OAuth2 Medium.
  • Patreon-go – библиотека Go для Patreon API.

Веб-фреймворки

  • Aero – высокопроизводительный веб-фреймворк для Go, который набирает высшие баллы в Lighthouse.
  • Banjo – очень простой и быстрый веб-фреймворк для Go.
  • Fiber – веб-фреймворк, вдохновленный Express.js и построенный на Fasthttp.
  • Flamingo Commerce – предоставление функций электронной коммерции с использованием чистой архитектуры (вроде DDD), а также портов и адаптеров, которые можно использовать для создания гибких приложений.
  • Gizmo – набор микросервисных, используемый New York Times.
  • Goyave – полнофункциональная инфраструктура REST API, нацеленная на чистый код и быструю разработку.
  • Microservice – фреймворк для создания микросервисов.
  • Rex – библиотека для модульной разработки, основанная на gorilla/mux и полностью совместимая с net/http.
  • WebGo – микро-фреймворк для создания веб-приложений

WebAssembly

  • Dom – библиотека DOM.
  • Go-canvas – библиотека для использования HTML5 Canvas в коде go.
  • Tinygo – компилятор Go: микроконтроллеры, WebAssembly и инструменты командной строки на основе LLVM.
  • Vert – взаимодействие между значениями Go и JS.
  • Wasmbrowsertest – запускает тесты Go WASM в вашем браузере.
  • Webapi – привязки для DOM и HTML, созданные из WebIDL.

Заключение

Наш далеко не полный список приложений, инструментов и библиотек призван проиллюстрировать возможности Golang для веб-разработки. Если вы решили освоить язык самостоятельно или улучшить свои знания, рекомендуем ознакомиться со списком из 30 лучших книг. Для более системного изучения стоит обратить внимание на курсы онлайн-академии GeekBrains. За 12 месяцев опытные преподаватели научат вас писать на Go высоконагруженные приложения и безопасные масштабируемые сервисы. График занятий (2 раза в неделю) идеально подходит не только для новичков, но для программистов, занимающихся еще одним языком в свободное время. Учебная программа ориентирована на практическую работу в команде, а успешно окончившие курс слушатели смогут добавить к портфолио 5 реализованных проектов и получат помощь в трудоустройстве.

10
Апр
2021

⚖ Golang против Python: какой язык программирования выбрать?

В этой статье попытаемся сравнить Python и Golang по различным характеристикам. Посмотрим, какой из этих языков стоит предпочесть новичку, только собирающемуся заняться программированием профессионально.

13
Мар
2021

🕵 Примеры атак XSS и способов их ослабления

Понимание межсайтового скриптинга и способов борьбы с ним необходимо каждому веб-разработчику. Это один из самых распространенных видов уязвимостей – злоумышленники часто проводят атаки XSS для кражи данных и нарушения работоспособности сервисов.

Перевод публикуется с сокращениями, автор оригинальной статьи Russel Jones.

Межсайтовый
скриптинг (
XSS)
– это атака, которая позволяет
JavaScript через
один сайт работать с другим.
XSS интересен не из-за технической
сложности, а скорее потому, что он эксплуатирует некоторые из основных
механизмов безопасности браузеров и из-за огромной распространенности.

Background

Изначально Всемирная Паутина представляла собой набор статических документов HTML, которые браузер должен был отображать для просмотра пользователями. По мере развития Интернета возрастали и требования к документам, что привело к появлению JavaScript и файлов cookie: скрипты нужны для интерактивности документа, а cookies – чтобы браузеры могли сохранять его состояние.

Появление этих возможностей привело к тому, что браузеры не только визуализируют HTML,
но и вмещают в памяти в качестве API для разработчиков представление,
называемое объектной моделью документа (DOM). DOM предлагает древовидную структуру тегов HTML, а также доступ к файлам cookie для
получения состояния. Со временем модель превратилась из предназначенной преимущественно для чтения структуры в структуру read-write, обновление которой приводит к повторному рендерингу документа.

Как только документы
получили возможность запускать код, браузеры должны были определить контекст
выполнения для программ на JavaScript. Политика, которая была разработана, называется
Same-Origin и по-прежнему является одним из фундаментальных примитивов безопасности
браузера. Изначально в ней утверждалось, что JavaScript в одном документе может
получить доступ только к собственному DOM и к DOM других документов с тем же
происхождением. Позже, когда был добавлен XMLHttpRequest
и Fetch, появилась модифицированная версия
Same-Origin. Эти API не могут
выдавать запросы к любому источнику, они могут только читать ответ на запросы
от того же источника.

Что же такое происхождение? Это кортеж протокола, имени хоста и порта документа.

Фрагмент 1: Кортеж из схемы, хоста и порта этого URL-адреса.
        https://www.example.com:443/app
^^^^^   ^^^^^^^^^^^^^^^ ^^^
Scheme  Host            Port
    
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.

Политика Same-Origin
отлично помогает смягчать атаки на статические сайты, как показано на рисунке выше.
Однако с атаками на динамические ресурсы, принимающие пользовательский ввод, ситуация
немного сложнее из-за смешивания кода и данных, которая позволяет злоумышленнику
выполнять контролируемый ввод в исходном документе.

Атаки XSS обычно бывают трех видов: рефлективными, хранимыми и основанными на DOM.

Рефлективные и хранимые
XSS-атаки принципиально одинаковы, поскольку полагаются на вредоносный ввод,
отправляемый на бекенд и представляющий этот ввод пользователю сервер.
Рефлективные XSS обычно возникают в виде злонамеренно созданной злоумышленником
ссылки, по которой затем переходит жертва. Хранимые XSS происходят, когда
злоумышленник загружает вредоносный ввод. Атаки на основе DOM отличаются тем,
что они происходят исключительно на стороне клиента и включают вредоносный ввод, манипулирующий DOM.

Примеры

Рефлективные атаки XSS

Ниже можно увидеть
простое веб-приложение на Go, которое отражает свой ввод (даже если
это вредоносный скрипт) обратно пользователю. Вы можете использовать это
приложение, сохранив его в файле xss1.go и запустив go run xss1.go.

Фрагмент 3: Пример веб-приложения с рефлективной (отраженной) XSS-атакой.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    messages, ok := r.URL.Query()["message"]
    if !ok {
       messages = []string{"hello, world"}
    }
    fmt.Fprintf(w, "<html><p>%v</p></html>", messages[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть
XSS-атаку, перейдите по уязвимому URL-адресу ниже.

        http://localhost:8080?message=<script>alert(1)</script>
    

Взгляните на источник: сервер вернул документ, который выглядит примерно так, как показано во фрагменте
4. Обратите внимание, как смешение кода и данных позволило произойти этой
атаке.

Фрагмент 4: Пример вывода уязвимого для XSS веб-приложения.
        <html>
  <p>
    <script>alert(1)</script>
  </p>
</html>
    

Этот пример может
показаться неправдоподобным, поскольку защита XSS была явно отключена. Эта ее форма основана на эвристике с обходными путями для
различных браузеров. Она была отключена для создания кроссбраузерных примеров,
иллюстрирующих основные концепции XSS-атак. Некоторые браузеры
удаляют эту защиту: например, в
Google Chrome 78
и выше вам не понадобится строка
w.Header().Set(“X-XSS-Protection”, “0”), чтобы атака сработала.

Хранимые XSS-атаки

Хранимые XSS-атаки похожи на рефлективные, но пэйлоад поступает из хранилища данных, а не из ввода непосредственно. Например, злоумышленник может загрузить в веб-приложение зловреда, который затем будет показан каждому авторизованному юзеру.

Ниже приведен простой чат, который иллюстрирует этот вид атак. Вы можете сохранить приложение в файле xss2.go и
запустить с помощью команды go run xss2.go.

Фрагмент 5: Хранимая XSS-атака.
        package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
%v
`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	var sb strings.Builder
	sb.WriteString("<ul>")
	for _, message := range db {
		sb.WriteString("<li>" + message + "</li>")
	}
	sb.WriteString("</ul>")

	fmt.Fprintf(w, tmpl, sb.String())
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть атаку
XSS, перейдите по ссылке http://localhost:8080 и введите сообщение <script>alert(1);</script>.

Атака делится на две
фазы:

  • пейлоад сохраняется в хранилище данных в функции storeHandler;
  • когда страница визуализируется во ViewHandler, пейлоад добавляется к выходным данным.

XSS-атаки на основе DOM

Такие атаки не связаны с бекендом и происходят исключительно на стороне клиента. Они интересны тем, что современные веб-приложения перемещают логику к клиенту, а атаки происходят, когда пользователь напрямую манипулирует DOM. Хорошей новостью для злоумышленников является то, что DOM имеет широкий спектр способов эксплуатации, наиболее популярными из которых являются innerHTML и document.write.

Ниже приведен пример обслуживающего статический контент веб-приложения. Код тот же, что и в
примере с рефлективными XSS, но здесь атака будет происходить полностью на
стороне клиента. Вы можете сохранить приложение в файле xss3.go и запустить его командой go run xss3.go.

Фрагмент 6: Пример веб-приложения с XSS-атакой на основе DOM.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

const content = `

<html>
   <head>
       <script>
          window.onload = function() {
             var params = new URLSearchParams(window.location.search);
             p = document.getElementById("content")
             p.innerHTML = params.get("message")
	     };
       </script>
   </head>
   <body>
       <p id="content"></p>
   </body>
</html>
`

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")
    fmt.Fprintf(w, content)
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть эту
атаку, перейдите по ссылке
http://localhost:8080/?message=”<img src=1 onerror=alert(1);/>”. Обратите внимание, что вектор атаки немного
отличается и innerHTML не будет
выполнять скрипт напрямую, однако он добавит HTML-элементы, которые затем выполнят код на JavaScript. В приведенном примере добавляется элемент image, который запускает скрипт при возникновении ошибки (она всегда появляется, поскольку злоумышленник подставляет неверный источник).

Если хотите напрямую добавить
элемент скрипта, придется использовать другой приемник XSS. Замените
элемент script из фрагмента 6 элементом script из фрагмента 7 и перейдите
по следующей ссылке:
http://localhost:8080/?message=”<script>alert(1);</script>”. Атака сработает, потому что document.write принимает элементы
скрипта напрямую.

Фрагмент 7: Еще один пример атаки XSS на основе DOM.
        <script>
   window.onload = function() {
      var params = new URLSearchParams(window.location.search);
      document.open();
      document.write(params.get("message"));
      document.close();
   };
</script>
    

Связанные направления атак

Хотя обычно их не называют атаками XSS, существует несколько связанных направлений, о которых стоит упомянуть.

Content-type

Всему виной
неправильная настройка типа содержимого ответов HTTP. Это может произойти как
на уровне бекенда (ответ имеет неверный набор заголовков Content-Type), так и при попытке браузера проснифферить тип MIME.
Internet Explorer был особенно восприимчив к этому, и классическим примером
является служба загрузки изображений: злоумышленник может загрузить JavaScript вместо картинки.
Браузер видит, что тип контента был установлен на image/jpg, но пейлоад содержит скрипт – он выполняется, что приводит к атаке XSS.

Urlschemes

Следующий тип атаки –
активность через URL со схемой JavaScript. Представим веб-сайт, который
позволяет пользователю контролировать цель ссылки, как показано во фрагменте 8. В этом случае злоумышленник сможет предоставить
URL, выполняющий некий JavaScript с помощью нашей схемы.

Чтобы опробовать этот
тип атаки, можно сохранить приложение в файле xss4.go, запустить командой go run xss4.go и перейти по ссылке http://localhost:8080?link=javascript:alert(1).

Фрагмент 8: XSS-атака, введенная через схему URL-адресов.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    links, ok := r.URL.Query()["link"]
    if !ok {
        messages = []string{"example.com"}
    }
    fmt.Fprintf(w, `<html><p><a href="%v">Next</p></html>`, links[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Избавление

Единого метода
решения данной проблемы не существует, иначе XSS не был бы
такой распространенной проблемой. Фундаментальная сложность вызвана отсутствием разделения между кодом и данными. Смягчение последствий XSS
обычно включает очистку входных данных (нужно убедиться, что они не
содержат кода), экранирование выходных данных (они также не должны содержать код) и реструктуризацию приложения таким образом, чтобы код
загружался из строго определенных конечных точек.

Валидация входных данных

Первая линия защиты – проверка входных данных. Убедитесь, что их формат соответствует ожидаемым характеристикам – эдакий белый список, гарантирующий отсутствие у приложения возможности принимать код.

Валидация
данных – сложная проблема. Не существует универсального инструмента или техники
для всех ситуаций. Лучше всего структурировать приложение таким образом, чтобы
оно требовало от разработчиков продумать тип принимаемых данных и обеспечить
удобное место, где можно разместить валидатор.

Хороший тон написания
приложений на Go состоит в том, чтобы не иметь никакой логики приложения в
обработчиках запросов HTTP, а вместо этого использовать их для анализа и проверки входных данных. Затем данные отправляются
в
обрабатывающую логику структуру. Обработчики запросов становятся
простыми и обеспечивают удобное централизованное расположение для контроля
правильности очистки данных.

На фрагменте 9
показано, как можно переписать saveHandler для приема символов ASCII [A-Za-z\.].

Фрагмент 9: Пример использования обработчиков HTTP-запросов для проверки данных.
        func saveHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	re := regexp.MustCompile(`^[A-Za-z\\.]+$`)
	if re.Find([]byte(messages[0]))) == "" {
		http.Error(w, "invalid message", 500)
	}
  
	db.Append(messages[0])

	http.Redirect(w, r, "/", 301)
}
    

Может показаться, что
это излишнее беспокойство, но чат-приложение принимает гораздо больше, чем ограниченный набор символов. Многие принимаемые приложениями данные достаточно структурированы: адреса, номера телефонов,
почтовые индексы и тому подобные вещи могут и должны быть
проверены.

Экранирование

Следующий шаг – экранирование вывода. В случае с нашим чатом все извлеченное из базы данных включалось непосредственно в выходной документ.

Одно и то же приложение
может быть гораздо безопаснее (даже если в него была произведена инъекция кода),
если экранировать все небезопасные выходные данные. Именно это делает пакет
html/template в Go. Использование языка шаблонов и
контекстно-зависимого синтаксического анализатора для экранирования данных до
их визуализации уменьшит вероятность выполнения вредоносного кода.

Ниже приведен пример
использования пакета html/template. Сохраните приложение в файле xss5.go, а затем выполните командой go run xss5.go.

Фрагмент 10: Использование экранирования для устранения хранимых XSS-атак.
        package main

import (
	"bytes"
	"html/template"
	"io"
	"log"
	"net/http"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
<ul>
{{range .}}
    <li>{{.}}</li>
{{end}}
</ul>`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	t := template.New("view")

	t, err := t.Parse(tmpl)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	var buf bytes.Buffer

	err = t.Execute(&buf, db)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	io.Copy(w, &buf)
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Опробуйте использованную ранее атаку XSS, перейдя по ссылке http://localhost:8080 и введите <script>alert(1);</script>.
Обратите внимание, что предупреждение не было вызвано.

Откройте консоль браузера и посмотрите на элемент li в DOM. Интерес представляют два
свойства: innerHTML и innerText.

Фрагмент 11: Проверка DOM при использовании экранирования.
        innerHTML: "<script>alert(1);</script>"
innerText: "<script>alert(1);</script>"
    

Обратите внимание, как
с помощью экранирования удалось четко разделить код и данные.

Content Security Policy

Content Security Policy (CSP) позволяет веб-приложениям определять набор доверенных источников для загрузки контента (например, скриптов). CSP можно использовать для разделения кода и данных, отказываясь от встроенных скриптов и загружая их только из определенных источников.

Написание CSP для
небольших автономных приложений является простой задачей – начните с политики,
которая по умолчанию запрещает все источники, а затем разрешите небольшой их набор.
Однако написать эффективный CSP для больших сайтов уже не так просто. Как
только сайт начинает загружать контент из внешних источников, CSP раздувается и
становится громоздким. Некоторые разработчики сдаются и включают директиву unsafe-inline, полностью разрушая теорию
CSP.

Чтобы упростить
написание CSP, в CSP3 вводится директива strict-dynamic.
Вместо того чтобы поддерживать большой белый список надежных источников,
приложение генерирует случайное число (nonce) каждый раз, когда запрашивается
страница. Этот nonce отправляется вместе с заголовками страницы и встроен в тег
script, что заставляет браузеры доверять этим скриптам с соответствующим nonce,
а также любым скриптам, которые они могут загрузить. Вместо
того, чтобы вносить скрипты в белый список и пытаться выяснить, какие еще сценарии они загружают, а затем пополнять белый список рекурсивно,
вам нужно достаточно внести в белый список импортируемый скрипт верхнего уровня.

Используя предложенный Google подход Strict CSP, рассмотрим простое приложение, принимающее
пользовательский ввод. Сохраните его в файле xss6.go, а затем выполните командой go run xss6.go.

Фрагмент 12: Пример CSP, смягчающего XSS-атаку.
        package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strings"
)

const scriptContent = `
document.addEventListener('DOMContentLoaded', function () {
   var updateButton = document.getElementById("textUpdate");
   updateButton.addEventListener("click", function() {
      var p = document.getElementById("content");
      var message = document.getElementById("textInput").value;
      p.innerHTML = message;
   });
};
`

const htmlContent = `
<html>
   <head>
      <script src="script.js" nonce="{{ . }}"></script>
   </head>
   <body>
       <p id="content"></p>

       <div class="input-group mb-3">
         <input type="text" class="form-control" id="textInput">
         <div class="input-group-append">
           <button class="btn btn-outline-secondary" type="button" id="textUpdate">Update</button>
         </div>
       </div>

       <blockquote class="twitter-tweet" data-lang="en">
         <a href="https://twitter.com/jack/status/20?ref_src=twsrc%5Etfw">March 21, 2006</a>
       </blockquote>
       <script async src="https://platform.twitter.com/widgets.js"
         nonce="{{ . }}" charset="utf-8"></script>
   </body>
</html>
`

func generateNonce() (string, error) {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(buf), nil
}

func generateHTML(nonce string) (string, error) {
	var buf bytes.Buffer

	t, err := template.New("htmlContent").Parse(htmlContent)
	if err != nil {
		return "", err
	}

	err = t.Execute(&buf, nonce)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func generatePolicy(nonce string) string {
	s := fmt.Sprintf(`'nonce-%v`, nonce) 
	var contentSecurityPolicy = []string{
		`object-src 'none';`,
		fmt.Sprintf(`script-src %v 'strict-dynamic';`, s),
		`base-uri 'none';`,
	}
	return strings.Join(contentSecurityPolicy, " ")
}

func scriptHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	fmt.Fprintf(w, scriptContent)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	htmlContent, err := generateHTML(nonce)
	if err != nil {
returnError()
		return
	}

	fmt.Fprintf(w, htmlContent)
}

func returnError() {
http.Error(w, http.StatusText(http.StatusInternalServerError),
		http.StatusInternalServerError)
}

func main() {
	http.HandleFunc("/script.js", scriptHandler)
	http.HandleFunc("/", htmlHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
    

Чтобы попытаться
использовать приложение, перейдите по ссылке: http://localhost:8080 и
попробуйте отправить <img src=1 onerror”alert(1)”/> как и раньше. Эта атака сработала бы и без CSP, но
поскольку CSP не допускает inline-скриптов, вы должны увидеть примерно такой вывод в консоли браузера:

«Отказано в выполнении встроенного обработчика событий, поскольку он нарушает следующую директиву CSP: “script-src ‘nonce-XauzABRw9QtE0bzoiRmslQ==’ ‘unsafe-inline’ ‘unsafe-eval’ ‘strict-dynamic’ https: http:” Обратите внимание, что ‘unsafe-inline‘ игнорируется, если в исходном списке присутствует либо хэш, либо значение nonce.»

Почему сценарий не
запустился? Рассмотрим
CSP подробнее.

Фрагмент 13: Базовый CSP. Nonce повторно генерируется для каждого запроса.
        script-src 'strict-dynamic' 'nonce-XauzABRw9QtE0bzoiRmslQ==';
object-src 'none';
base-uri 'none';
    
Что делает эта политика? Директива script-src включает strict-dynamic и значение nonce, используемое для загрузки скриптов. Это означает, что единственные скрипты, которые будут загружены, находятся в script elements, где nonce включен в атрибут, а значит inline-скрипты не загрузятся. Последние две директивы препятствуют загрузке плагинов и изменению базового URL приложения.

Основная сложность
использования этого подхода заключается в необходимости генерировать nonce и инжектить его в заголовки при каждой загрузке страницы. После этого шаблон
может быть применен ко всем загружаемым страницам.

Соответствующие методы устранения

Content-Type

Вы
должны не только устанавливать свой Content-Type, но и следить, чтобы браузеры не
пытались автоматически определить тип контента. Для
этого используйте заголовок: X-Content-Type-Options:
nosniff
.

Virtual doms

Хотя виртуальные домены
не являются функцией безопасности, использующие их современные фреймворки (
React и Vue) могут помочь смягчить атаки XSS на основе DOM.

Эти фреймворки создают DOM параллельно с тем, который находится в браузере, и сравнивают их. Отличающуюся часть DOM браузера они обновляют. Для этого необходимо создать виртуальный DOM, что приведет к уменьшению использования клиентами innerHTML и подтолкнет разработчиков к переходу на innerText.

React требует
использования атрибута
dangerouslySetInnerHTML, в то время как создатели Vue предупреждают, что использование
innerHTML может привести к появлению уязвимостей.

Заключение

Если вы дочитали до
конца, у вас может появиться желание разобраться, как работают браузеры, что такое ошибки
XSS и насколько важно понимать, как от них избавиться.
XSS трудно искоренить, поскольку приложения становятся все больше и все сложнее. Применяя упомянутые в статье методы, можно сделать жизнь злоумышленников трудной.

Удачи в борьбе и учебе!

Дополнительные материалы:

13
Мар
2021

🕵 Примеры атак XSS и способов их устранения

Понимание межсайтового скриптинга и способов борьбы с ним необходимо каждому веб-разработчику. Это один и самых распространенных видов уязвимостей – злоумышленники часто проводят атаки XSS для кражи данных и нарушения работоспособности сервисов.

Перевод публикуется с сокращениями, автор оригинальной статьи Russel Jones.

Межсайтовый
скриптинг (
XSS)
– это атака, которая позволяет
JavaScript через
один сайт работать с другим.
XSS интересен не из-за технической
сложности, а скорее потому, что он эксплуатирует некоторые из основных
механизмов безопасности браузеров и из-за огромной распространенности.

Background

Изначально Всемирная Паутина представляла собой набор статических документов HTML, которые браузер должен был отображать для просмотра пользователями. По мере развития Интернета возрастали и требования к документам, что привело к появлению JavaScript и файлов cookie: скрипты нужны для интерактивности документа, а cookies – чтобы браузеры могли сохранять его состояние.

Появление этих возможностей привело к тому, что браузеры не только визуализируют HTML,
но и вмещают в памяти в качестве API для разработчиков представление,
называемое объектной моделью документа (DOM). DOM предлагает древовидную структуру тегов HTML, а также доступ к файлам cookie для
получения состояния. Со временем модель превратилась из предназначенной преимущественно для чтения структуры в структуру read-write, обновление которой приводит к повторному рендерингу документа.

Как только документы
получили возможность запускать код, браузеры должны были определить контекст
выполнения для программ на JavaScript. Политика, которая была разработана, называется
Same-Origin и по-прежнему является одним из фундаментальных примитивов безопасности
браузера. Изначально в ней утверждалось, что JavaScript в одном документе может
получить доступ только к собственному DOM и к DOM других документов с тем же
происхождением. Позже, когда был добавлен XMLHttpRequest
и Fetch, появилась модифицированная версия
Same-Origin. Эти API не могут
выдавать запросы к любому источнику, они могут только читать ответ на запросы
от того же источника.

Что же такое происхождение? Это кортеж протокола, имени хоста и порта документа.

Фрагмент 1: Кортеж из схемы, хоста и порта этого URL-адреса.
        https://www.example.com:443/app
^^^^^   ^^^^^^^^^^^^^^^ ^^^
Scheme  Host            Port
    
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.
Рис. 1: Иллюстрация Same-Origin в действии. JavaScript работает на www.evil.com и не может получить доступ к DOM на www.example.com.

Политика Same-Origin
отлично помогает смягчать атаки на статические сайты, как показано на рисунке выше.
Однако с атаками на динамические ресурсы, принимающие пользовательский ввод, ситуация
немного сложнее из-за смешивания кода и данных, которая позволяет злоумышленнику
выполнять контролируемый ввод в исходном документе.

Атаки XSS обычно бывают трех видов: рефлективными, хранимыми и основанными на DOM.

Рефлективные и хранимые
XSS-атаки принципиально одинаковы, поскольку полагаются на вредоносный ввод,
отправляемый на бекенд и представляющий этот ввод пользователю сервер.
Рефлективные XSS обычно возникают в виде злонамеренно созданной злоумышленником
ссылки, по которой затем переходит жертва. Хранимые XSS происходят, когда
злоумышленник загружает вредоносный ввод. Атаки на основе DOM отличаются тем,
что они происходят исключительно на стороне клиента и включают вредоносный ввод, манипулирующий DOM.

Примеры

Рефлективные атаки XSS

Ниже можно увидеть
простое веб-приложение на Go, которое отражает свой ввод (даже если
это вредоносный скрипт) обратно пользователю. Вы можете использовать это
приложение, сохранив его в файле xss1.go и запустив go run xss1.go.

Фрагмент 3: Пример веб-приложения с рефлективной (отраженной) XSS-атакой.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    messages, ok := r.URL.Query()["message"]
    if !ok {
       messages = []string{"hello, world"}
    }
    fmt.Fprintf(w, "<html><p>%v</p></html>", messages[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть
XSS-атаку, перейдите по уязвимому URL-адресу ниже.

        http://localhost:8080?message=<script>alert(1)</script>
    

Взгляните на источник: сервер вернул документ, который выглядит примерно так, как показано во фрагменте
4. Обратите внимание, как смешение кода и данных позволило произойти этой
атаке.

Фрагмент 4: Пример вывода уязвимого для XSS веб-приложения.
        <html>
  <p>
    <script>alert(1)</script>
  </p>
</html>
    

Этот пример может
показаться неправдоподобным, поскольку защита XSS была явно отключена. Эта ее форма основана на эвристике с обходными путями для
различных браузеров. Она была отключена для создания кроссбраузерных примеров,
иллюстрирующих основные концепции XSS-атак. Некоторые браузеры
удаляют эту защиту: например, в
Google Chrome 78
и выше вам не понадобится строка
w.Header().Set(“X-XSS-Protection”, “0”), чтобы атака сработала.

Хранимые XSS-атаки

Хранимые XSS-атаки похожи на рефлективные, но пэйлоад поступает из хранилища данных, а не из ввода непосредственно. Например, злоумышленник может загрузить в веб-приложение зловреда, который затем будет показан каждому авторизованному юзеру.

Ниже приведен простой чат, который иллюстрирует этот вид атак. Вы можете сохранить приложение в файле xss2.go и
запустить с помощью команды go run xss2.go.

Фрагмент 5: Хранимая XSS-атака.
        package main

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
%v
`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	var sb strings.Builder
	sb.WriteString("<ul>")
	for _, message := range db {
		sb.WriteString("<li>" + message + "</li>")
	}
	sb.WriteString("</ul>")

	fmt.Fprintf(w, tmpl, sb.String())
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть атаку
XSS, перейдите по ссылке http://localhost:8080 и введите сообщение <script>alert(1);</script>.

Атака делится на две
фазы:

  • пейлоад сохраняется в хранилище данных в функции storeHandler;
  • когда страница визуализируется во ViewHandler, пейлоад добавляется к выходным данным.

XSS-атаки на основе DOM

Такие атаки не связаны с бекендом и происходят исключительно на стороне клиента. Они интересны тем, что современные веб-приложения перемещают логику к клиенту, а атаки происходят, когда пользователь напрямую манипулирует DOM. Хорошей новостью для злоумышленников является то, что DOM имеет широкий спектр способов эксплуатации, наиболее популярными из которых являются innerHTML и document.write.

Ниже приведен пример обслуживающего статический контент веб-приложения. Код тот же, что и в
примере с рефлективными XSS, но здесь атака будет происходить полностью на
стороне клиента. Вы можете сохранить приложение в файле xss3.go и запустить его командой go run xss3.go.

Фрагмент 6: Пример веб-приложения с XSS-атакой на основе DOM.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

const content = `

<html>
   <head>
       <script>
          window.onload = function() {
             var params = new URLSearchParams(window.location.search);
             p = document.getElementById("content")
             p.innerHTML = params.get("message")
	     };
       </script>
   </head>
   <body>
       <p id="content"></p>
   </body>
</html>
`

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")
    fmt.Fprintf(w, content)
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Чтобы увидеть эту
атаку, перейдите по ссылке
http://localhost:8080/?message=”<img src=1 onerror=alert(1);/>”. Обратите внимание, что вектор атаки немного
отличается и innerHTML не будет
выполнять скрипт напрямую, однако он добавит HTML-элементы, которые затем выполнят код на JavaScript. В приведенном примере добавляется элемент image, который запускает скрипт при возникновении ошибки (она всегда появляется, поскольку злоумышленник подставляет неверный источник).

Если хотите напрямую добавить
элемент скрипта, придется использовать другой приемник XSS. Замените
элемент script из фрагмента 6 элементом script из фрагмента 7 и перейдите
по следующей ссылке:
http://localhost:8080/?message=”<script>alert(1);</script>”. Атака сработает, потому что document.write принимает элементы
скрипта напрямую.

Фрагмент 7: Еще один пример атаки XSS на основе DOM.
        <script>
   window.onload = function() {
      var params = new URLSearchParams(window.location.search);
      document.open();
      document.write(params.get("message"));
      document.close();
   };
</script>
    

Связанные направления атак

Хотя обычно их не называют атаками XSS, существует несколько связанных направлений, о которых стоит упомянуть.

Content-type

Всему виной
неправильная настройка типа содержимого ответов HTTP. Это может произойти как
на уровне бекенда (ответ имеет неверный набор заголовков Content-Type), так и при попытке браузера проснифферить тип MIME.
Internet Explorer был особенно восприимчив к этому, и классическим примером
является служба загрузки изображений: злоумышленник может загрузить JavaScript вместо картинки.
Браузер видит, что тип контента был установлен на image/jpg, но пейлоад содержит скрипт – он выполняется, что приводит к атаке XSS.

Urlschemes

Следующий тип атаки –
активность через URL со схемой JavaScript. Представим веб-сайт, который
позволяет пользователю контролировать цель ссылки, как показано во фрагменте 8. В этом случае злоумышленник сможет предоставить
URL, выполняющий некий JavaScript с помощью нашей схемы.

Чтобы опробовать этот
тип атаки, можно сохранить приложение в файле xss4.go, запустить командой go run xss4.go и перейти по ссылке http://localhost:8080?link=javascript:alert(1).

Фрагмент 8: XSS-атака, введенная через схему URL-адресов.
        package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-XSS-Protection", "0")

    links, ok := r.URL.Query()["link"]
    if !ok {
        messages = []string{"example.com"}
    }
    fmt.Fprintf(w, `<html><p><a href="%v">Next</p></html>`, links[0])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Избавление

Единого метода
решения данной проблемы не существует, иначе XSS не был бы
такой распространенной проблемой. Фундаментальная сложность вызвана отсутствием разделения между кодом и данными. Смягчение последствий XSS
обычно включает очистку входных данных (нужно убедиться, что они не
содержат кода), экранирование выходных данных (они также не должны содержать код) и реструктуризацию приложения таким образом, чтобы код
загружался из строго определенных конечных точек.

Валидация входных данных

Первая линия защиты – проверка входных данных. Убедитесь, что их формат соответствует ожидаемым характеристикам – эдакий белый список, гарантирующий отсутствие у приложения возможности принимать код.

Валидация
данных – сложная проблема. Не существует универсального инструмента или техники
для всех ситуаций. Лучше всего структурировать приложение таким образом, чтобы
оно требовало от разработчиков продумать тип принимаемых данных и обеспечить
удобное место, где можно разместить валидатор.

Хороший тон написания
приложений на Go состоит в том, чтобы не иметь никакой логики приложения в
обработчиках запросов HTTP, а вместо этого использовать их для анализа и проверки входных данных. Затем данные отправляются
в
обрабатывающую логику структуру. Обработчики запросов становятся
простыми и обеспечивают удобное централизованное расположение для контроля
правильности очистки данных.

На фрагменте 9
показано, как можно переписать saveHandler для приема символов ASCII [A-Za-z\.].

Фрагмент 9: Пример использования обработчиков HTTP-запросов для проверки данных.
        func saveHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	re := regexp.MustCompile(`^[A-Za-z\\.]+$`)
	if re.Find([]byte(messages[0]))) == "" {
		http.Error(w, "invalid message", 500)
	}
  
	db.Append(messages[0])

	http.Redirect(w, r, "/", 301)
}
    

Может показаться, что
это излишнее беспокойство, но чат-приложение принимает гораздо больше, чем ограниченный набор символов. Многие принимаемые приложениями данные достаточно структурированы: адреса, номера телефонов,
почтовые индексы и тому подобные вещи могут и должны быть
проверены.

Экранирование

Следующий шаг – экранирование вывода. В случае с нашим чатом все извлеченное из базы данных включалось непосредственно в выходной документ.

Одно и то же приложение
может быть гораздо безопаснее (даже если в него была произведена инъекция кода),
если экранировать все небезопасные выходные данные. Именно это делает пакет
html/template в Go. Использование языка шаблонов и
контекстно-зависимого синтаксического анализатора для экранирования данных до
их визуализации уменьшит вероятность выполнения вредоносного кода.

Ниже приведен пример
использования пакета html/template. Сохраните приложение в файле xss5.go, а затем выполните командой go run xss5.go.

Фрагмент 10: Использование экранирования для устранения хранимых XSS-атак.
        package main

import (
	"bytes"
	"html/template"
	"io"
	"log"
	"net/http"
	"sync"
)

var db []string
var mu sync.Mutex

var tmpl = `
<form action="/save">
  Message: <input name="message" type="text"><br>
  <input type="submit" value="Submit">
</form>
<ul>
{{range .}}
    <li>{{.}}</li>
{{end}}
</ul>`

func saveHandler(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	defer mu.Unlock()

	r.ParseForm()
	messages, ok := r.Form["message"]
	if !ok {
		http.Error(w, "missing message", 500)
	}

	db = append(db, messages[0])

	http.Redirect(w, r, "/", 301)
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	t := template.New("view")

	t, err := t.Parse(tmpl)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	var buf bytes.Buffer

	err = t.Execute(&buf, db)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	io.Copy(w, &buf)
}

func main() {
	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/save", saveHandler)
	log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
    

Опробуйте использованную ранее атаку XSS, перейдя по ссылке http://localhost:8080 и введите <script>alert(1);</script>.
Обратите внимание, что предупреждение не было вызвано.

Откройте консоль браузера и посмотрите на элемент li в DOM. Интерес представляют два
свойства: innerHTML и innerText.

Фрагмент 11: Проверка DOM при использовании экранирования.
        innerHTML: "<script>alert(1);</script>"
innerText: "<script>alert(1);</script>"
    

Обратите внимание, как
с помощью экранирования удалось четко разделить код и данные.

Content Security Policy

Content Security Policy (CSP) позволяет веб-приложениям определять набор доверенных источников для загрузки контента (например, скриптов). CSP можно использовать для разделения кода и данных, отказываясь от встроенных скриптов и загружая их только из определенных источников.

Написание CSP для
небольших автономных приложений является простой задачей – начните с политики,
которая по умолчанию запрещает все источники, а затем разрешите небольшой их набор.
Однако написать эффективный CSP для больших сайтов уже не так просто. Как
только сайт начинает загружать контент из внешних источников, CSP раздувается и
становится громоздким. Некоторые разработчики сдаются и включают директиву unsafe-inline, полностью разрушая теорию
CSP.

Чтобы упростить
написание CSP, в CSP3 вводится директива strict-dynamic.
Вместо того чтобы поддерживать большой белый список надежных источников,
приложение генерирует случайное число (nonce) каждый раз, когда запрашивается
страница. Этот nonce отправляется вместе с заголовками страницы и встроен в тег
script, что заставляет браузеры доверять этим скриптам с соответствующим nonce,
а также любым скриптам, которые они могут загрузить. Вместо
того, чтобы вносить скрипты в белый список и пытаться выяснить, какие еще сценарии они загружают, а затем пополнять белый список рекурсивно,
вам нужно достаточно внести в белый список импортируемый скрипт верхнего уровня.

Используя предложенный Google подход Strict CSP, рассмотрим простое приложение, принимающее
пользовательский ввод. Сохраните его в файле xss6.go, а затем выполните командой go run xss6.go.

Фрагмент 12: Пример CSP, смягчающего XSS-атаку.
        package main

import (
	"bytes"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"strings"
)

const scriptContent = `
document.addEventListener('DOMContentLoaded', function () {
   var updateButton = document.getElementById("textUpdate");
   updateButton.addEventListener("click", function() {
      var p = document.getElementById("content");
      var message = document.getElementById("textInput").value;
      p.innerHTML = message;
   });
};
`

const htmlContent = `
<html>
   <head>
      <script src="script.js" nonce="{{ . }}"></script>
   </head>
   <body>
       <p id="content"></p>

       <div class="input-group mb-3">
         <input type="text" class="form-control" id="textInput">
         <div class="input-group-append">
           <button class="btn btn-outline-secondary" type="button" id="textUpdate">Update</button>
         </div>
       </div>

       <blockquote class="twitter-tweet" data-lang="en">
         <a href="https://twitter.com/jack/status/20?ref_src=twsrc%5Etfw">March 21, 2006</a>
       </blockquote>
       <script async src="https://platform.twitter.com/widgets.js"
         nonce="{{ . }}" charset="utf-8"></script>
   </body>
</html>
`

func generateNonce() (string, error) {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		return "", err
	}

	return base64.StdEncoding.EncodeToString(buf), nil
}

func generateHTML(nonce string) (string, error) {
	var buf bytes.Buffer

	t, err := template.New("htmlContent").Parse(htmlContent)
	if err != nil {
		return "", err
	}

	err = t.Execute(&buf, nonce)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func generatePolicy(nonce string) string {
	s := fmt.Sprintf(`'nonce-%v`, nonce) 
	var contentSecurityPolicy = []string{
		`object-src 'none';`,
		fmt.Sprintf(`script-src %v 'strict-dynamic';`, s),
		`base-uri 'none';`,
	}
	return strings.Join(contentSecurityPolicy, " ")
}

func scriptHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	fmt.Fprintf(w, scriptContent)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
	nonce, err := generateNonce()
	if err != nil {
		returnError()
		return
	}

	w.Header().Set("X-XSS-Protection", "0")
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Content-Security-Policy", generatePolicy(nonce))

	htmlContent, err := generateHTML(nonce)
	if err != nil {
returnError()
		return
	}

	fmt.Fprintf(w, htmlContent)
}

func returnError() {
http.Error(w, http.StatusText(http.StatusInternalServerError),
		http.StatusInternalServerError)
}

func main() {
	http.HandleFunc("/script.js", scriptHandler)
	http.HandleFunc("/", htmlHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
    

Чтобы попытаться
использовать приложение, перейдите по ссылке: http://localhost:8080 и
попробуйте отправить <img src=1 onerror”alert(1)”/> как и раньше. Эта атака сработала бы и без CSP, но
поскольку CSP не допускает inline-скриптов, вы должны увидеть примерно такой вывод в консоли браузера:

«Отказано в выполнении встроенного обработчика событий, поскольку он нарушает следующую директиву CSP: “script-src ‘nonce-XauzABRw9QtE0bzoiRmslQ==’ ‘unsafe-inline’ ‘unsafe-eval’ ‘strict-dynamic’ https: http:” Обратите внимание, что ‘unsafe-inline‘ игнорируется, если в исходном списке присутствует либо хэш, либо значение nonce.»

Почему сценарий не
запустился? Рассмотрим
CSP подробнее.

Фрагмент 13: Базовый CSP. Nonce повторно генерируется для каждого запроса.
        script-src 'strict-dynamic' 'nonce-XauzABRw9QtE0bzoiRmslQ==';
object-src 'none';
base-uri 'none';
    
Что делает эта политика? Директива script-src включает strict-dynamic и значение nonce, используемое для загрузки скриптов. Это означает, что единственные скрипты, которые будут загружены, находятся в script elements, где nonce включен в атрибут, а значит inline-скрипты не загрузятся. Последние две директивы препятствуют загрузке плагинов и изменению базового URL приложения.

Основная сложность
использования этого подхода заключается в необходимости генерировать nonce и инжектить его в заголовки при каждой загрузке страницы. После этого шаблон
может быть применен ко всем загружаемым страницам.

Соответствующие методы устранения

Content-Type

Вы
должны не только устанавливать свой Content-Type, но и следить, чтобы браузеры не
пытались автоматически определить тип контента. Для
этого используйте заголовок: X-Content-Type-Options:
nosniff
.

Virtual doms

Хотя виртуальные домены
не являются функцией безопасности, использующие их современные фреймворки (
React и Vue) могут помочь смягчить атаки XSS на основе DOM.

Эти фреймворки создают DOM параллельно с тем, который находится в браузере, и сравнивают их. Отличающуюся часть DOM браузера они обновляют. Для этого необходимо создать виртуальный DOM, что приведет к уменьшению использования клиентами innerHTML и подтолкнет разработчиков к переходу на innerText.

React требует
использования атрибута
dangerouslySetInnerHTML, в то время как создатели Vue предупреждают, что использование
innerHTML может привести к появлению уязвимостей.

Заключение

Если вы дочитали до
конца, у вас может появиться желание разобраться, как работают браузеры, что такое ошибки
XSS и насколько важно понимать, как от них избавиться.
XSS трудно искоренить, поскольку приложения становятся все больше и все сложнее. Применяя упомянутые в статье методы, можно сделать жизнь злоумышленников трудной.

Удачи в борьбе и учебе!

Дополнительные материалы:

24
Фев
2021

🏃 Где используется язык программирования Go?

Говорят, будто Golang – язык будущего. Он простой, эффективный и очень дружественный к новичкам. В мини-обзоре попробуем разобраться с основными особенностями и сферой применения набирающего популярность языка программирования.

26
Дек
2020

🏃 Конкурентность в Golang и WorkerPool [Часть 2]

В первой статье мы строили Worker Pool для оптимизации производительности. Во второй части мы создадим надежное решение для работы со структурами конкурентности.

Перевод публикуетс…

26
Дек
2020

🏃 Параллелизм в Golang и WorkerPool [Часть 2]

В первой статье мы строили Worker Pool для оптимизации производительности. Во второй части мы создадим надежное решение для работы со структурами параллелизма.

Перевод публикуется …

24
Дек
2020

🏃 Параллелизм в Golang и WorkerPool [Часть 1]

В современных языках программирования параллелизм стал безусловной потребностью. В этой статье речь пойдет об устройстве и использовании concurrency в Go.

Перевод публикуется с сок…

27
Сен
2020

Книги по языку Go с лучшими оценками читателей. Расставлены в порядке возрастания сложности, обобщены указанные читателями преимущества и недостатки.

Книги по языку Go для началь…

21
Июн
2020

Cортировки и битовые маски, обработка ошибок и создание изображений, генерация перестановок и работа с хэш-суммами, запуск HTTP-сервера, юнит-тесты и другие распространенные задачи, решаемые с помощью Go.

28
Май
2020

Рассказываем о методах инструментирования Go-кода, контекстной трассировке и специальном средстве лаконичного и гибкого инструментирования gtrace.

76331ef0-7c12-4d04-9ce9-fc153ee…

15
Янв
2020

Опыт разработки: почему мы пишем инфраструктуру машинного обучения на Go, а не на Python

Разработчик Cortex Калеб Кайзер делится соображениями о преимуществах применения Go для инфраструктурных решений в ML-проектах и о том, как два языка могут дополнять друг друга.

Python – самый популярный яз…

06
Ноя
2019

Трансляция HighLoad++ 2019

В программе: архитектура и эксплуатация проектов, базы данных и системы хранения, системное администрирование, нагрузочное тестирование и не только.
— Читать дальше «Трансляция HighLoad++ 2019»