modify resources of an exe

Конфигурация по умолчанию в exe файле, ее изменение и сохранение в ресурсе типа RT_RCDATA, всё официальными функциями BeginUpdateResource, UpdateResource, EndUpdateResource, без хакерства.
	path = GetCommandLine();
	path = path.Mid(1); // убираем "
	path = path.Left(path.ReverseFind('\\'));
	path += L"\\server.exe";

	try {
		// проверяем наличие экзешника, в моем случае должен лежать в одном каталоге с патчилкой
		CFileException* e = new CFileException();
		CFile cfile;
		if (!cfile.Open(path, CFile::modeReadWrite, e))
		{
			throw e;
		}
		cfile.Close();

		LPBYTE bomPos = 0;
		// загрузка ресурса
		HMODULE hLibrary = 0;
		HRSRC hResource = 0;
		HGLOBAL hResourceLoaded = 0;
		DWORD  resourceSize = 0;
		LPBYTE lpBuffer = 0;
		hLibrary = LoadLibrary(path);
		if (NULL != hLibrary)
		{
			hResource = FindResource(hLibrary, MAKEINTRESOURCE(paramsResId), RT_RCDATA); // !!! paramsResId == 139 ИД ресурса в экзешнике
			if (NULL != hResource)
			{
				hResourceLoaded = LoadResource(hLibrary, hResource);
				resourceSize = SizeofResource(hLibrary, hResource);
				if (NULL != hResourceLoaded)
				{
					lpBuffer = (LPBYTE)LockResource(hResourceLoaded);
					bomPos = new BYTE[resourceSize];
					memcpy_s(bomPos, resourceSize, lpBuffer, resourceSize);
				}
			}
			FreeLibrary(hLibrary);
		}

		if (bomPos == 0) {
			AfxThrowArchiveException(CArchiveException::endOfFile, _T("signature"));
		}

		// попытка загрузить ранее записанные параметры		
		int sizeOfParams = (int)*(bomPos + bomSize);
		if (sizeOfParams > 0 && sizeOfParams < 800) {
			BYTE kk[] = { 'q', 10, 'e', 7, 'a', 'z', 'Q' };
			encryptDecrypt((BYTE*)(bomPos + bomSize + sizeof(0)), sizeOfParams, kk, sizeof(kk));
			int realLength = wcslen((TCHAR*)(bomPos + bomSize + sizeof(0)));
			TCHAR* decryptData = (TCHAR*)(bomPos + bomSize + sizeof(0));
			if (realLength > 2 && decryptData[0] == _T('Z') && decryptData[1] == _T('A')) { // проверяем на ZA, значит всё раскодировалось
				decryptData += 2; // пропустим ZA
				TCHAR* s_ssh_host = 0, *s_ssh_port = 0, *s_ssh_user = 0, *s_ssh_pwd = 0, *s_ssh_add = 0;
				s_ssh_host = decryptData;
				s_ssh_port = _tcschr(s_ssh_host, _T('\t')) + 1;
				if (s_ssh_port)
					s_ssh_user = _tcschr(s_ssh_port, _T('\t')) + 1;
				if (s_ssh_user)
					s_ssh_pwd = _tcschr(s_ssh_user, _T('\t')) + 1;
				if (s_ssh_pwd)
					s_ssh_add = _tcschr(s_ssh_pwd, _T('\t')) + 1;
				if (s_ssh_port)
					*(s_ssh_port - 1) = 0;
				if (s_ssh_user)
					*(s_ssh_user - 1) = 0;
				if (s_ssh_pwd)
					*(s_ssh_pwd - 1) = 0;
				if (s_ssh_add)
					*(s_ssh_add - 1) = 0;
			
				//
			}
		}
		delete[] bomPos;
	CString s_ssh_host;
	ssh_host.GetWindowText(s_ssh_host);
	CString s_ssh_port;
	ssh_port.GetWindowText(s_ssh_port);
	CString s_ssh_user;
	ssh_user.GetWindowText(s_ssh_user);
	CString s_ssh_pwd;
	ssh_pwd.GetWindowText(s_ssh_pwd);
	CString s_ssh_add;
	ssh_add.GetWindowText(s_ssh_add);
	CString s_full;
	s_full.Format(_T("ZA%s\t%s\t%s\t%s\t%s\0"), s_ssh_host.GetBuffer(), s_ssh_port.GetBuffer(), s_ssh_user.GetBuffer(), s_ssh_pwd.GetBuffer(), s_ssh_add.GetBuffer());

	HANDLE hResource;
	hResource = BeginUpdateResource(path, FALSE);
	if (NULL != hResource)
	{
		int sizeOfParams = (s_full.GetLength() + 1) * sizeof(_T('\0'));
		if (sizeOfParams > 700) {
			::AfxMessageBox(_T("Only 700 bytes allowed for writing."));
			FreeResource(hResource);
			return;
		}
		LPBYTE lpBuffer = new BYTE[bomSize + sizeof(0) + sizeOfParams];
		memcpy(lpBuffer, bom, bomSize);
		memcpy(lpBuffer + bomSize, &sizeOfParams, sizeof(0));
		memcpy(lpBuffer + bomSize + sizeof(0), (BYTE*)s_full.GetBuffer(), sizeOfParams);
		
		BYTE kk[] = { 'q', 10, 'e', 7, 'a', 'z', 'Q' };
		encryptDecrypt(lpBuffer + bomSize + sizeof(0), sizeOfParams, kk, sizeof(kk));
		
		if (UpdateResource(hResource,
			RT_RCDATA,
			MAKEINTRESOURCE(paramsResId),
			0,
			(LPVOID)lpBuffer,
			bomSize + sizeof(0) + sizeOfParams) != FALSE)
		{
			EndUpdateResource(hResource, FALSE);
		}
		delete[] lpBuffer;
	}

ssh over https proxy

Очередной раз из разряда, «век живи, век учись» или «если бы мы читали документацию».
Проброс портов с помощью PUTTY и туда и обратно и все это через корпоративный прокси сервер (TMG, NAT закрыт совсем).
Благо прокси разрешает Basic авторизацию, PUTTY только с ней и может, иначе было бы сложнее.
Wireshark По умолчанию в Threat Management Gateway для туннелирования с использованием HTTP метода CONNECT открыты всего два порта:
By default on TMG/ISA, the following tunnel ranges are configured:
NNTP (single port): 563
ssl (single port): 443
Воспользуемся портом 563 (443 у меня занят) и поднимем SSH сервер во вне (в моем случае проброс на домашнем роутере в одну из виртуалок)
Не забываем на SSH сервере разрешить AllowTcpForwarding yes и GatewayPorts yes
putty putty putty
По какой-то причине PUTTY принимает только 127.0.0.1 при обратном пробросе, пришлось воспользоваться помощью администратора
netsh interface portproxy add v4tov4 listenport=3393 listenaddress=0.0.0.0 connectport=3389 connectaddress=10.19.80.152
netsh interface portproxy add v4tov4 listenport=3397 listenaddress=0.0.0.0 connectport=80 connectaddress=10.19.80.76

Итого, с рабочего доступна RDP домашнего, с домашнего, RDP одного из серверов и веб-сервер из корпоративной сети.

Node, Puppeteer: автоматизация Google Chrome. Сохраняем ts файлы и преобразуем в mp4.

Используем именно Google Chrome, так как в Chromium не все работает, например, Shaka player (Google).
После установки Node.js
sudo dnf module list nodejs
sudo dnf module enable nodejs:12
sudo dnf install nodejs
Устанавливаем puppeteer без Chromium (350 МВ)
echo "export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true && npm install i puppeteer" | sudo sh
При запуске puppeteer указываем путь к исполняемому файлу Google Chrome
 executablePath: '/opt/google/chrome/google-chrome',
Выключаем CORS
        '--disable-web-security',
        '--disable-site-isolation-trials',
Из интересного для сохранения «.m3u8» и «.ts» используем page.evaluate, далее fetch в контексте браузера, затем передаем содержимое файла в puppeteer
let dataString = '[' + (new Uint8Array(data)).toString() + ']';
и сохраняем файл определенной с помощью page.exposeFunction функцией writefile.
'use strict';
const puppeteer = require('puppeteer');
const fs = require('fs');

(async function main() {
  var browser;
  try {

    var myArgs = process.argv.slice(2);
    if (!myArgs.length) {
        throw('need dir');        
    }
    console.log('dir: ', myArgs[0]);


    browser = await puppeteer.launch({
      headless: true,
      slowMo: 250, // slow down by 250ms
//      devtools: true,
      executablePath: '/opt/google/chrome/google-chrome',
      defaultViewport: null, // иначе не работает --window-size=1920,1080
      args:[
        '--window-size=1920,1080',
        '--disable-setuid-sandbox',
	'--no-sandbox',
        '--disable-infobars',
        '--disable-web-security',
        '--disable-site-isolation-trials',
        ],
    });

/*
    var lessonUrl = myArgs[0]

    const crypto = require('crypto');
    const hash = crypto.createHash('sha256');
    hash.update(lessonUrl);

    var dir = 'fdb' + '/' + hash.digest('hex');
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir);
    }
    var dirOriginal = dir + '/' + 'original';

    if (!fs.existsSync(dirOriginal)) {
        fs.mkdirSync(dirOriginal);
    }
*/

    var lessonUrl = myArgs[1];
    var dir = myArgs[0];
    var dirOriginal = dir + '/' + 'original';


    const regUrl = 'https://zzz.ru/user/registration'
    const page = await browser.newPage()
    page.on('console', msg => console.log('PAGE LOG:', msg.text()));
//    await page.setDefaultNavigationTimeout(60000);
    await page.goto(regUrl, { waitUntil: 'load' })
    await page.waitForTimeout(1000);
    await page.screenshot({ path: dir + '/' + 'screenshot01.png' })

    let checkUrl = await page.evaluate(() => location.href);
    if (regUrl == checkUrl) {
        await page.click('[data-qa="__authEmailButton"]')
        await page.type('#email', 'username@mail.ru')
        await page.type('#password', '12wdnj4ss7ty')
        await page.screenshot({ path: dir + '/' + 'screenshot02.png' })

        await Promise.all([
          page.click('[data-qa="__authEmailSubmitButton"]'),
          page.waitForNavigation(), // if "await page.waitForNavigation" then "TimeoutError: Navigation timeout of 30000 ms exceeded", причем на ноде поновее всё работало
        ]);
    }
    await page.waitForTimeout(2000);
    await page.screenshot({ path: dir + '/' + 'screenshot1.png' })
    console.log(lessonUrl);

    await page.goto(lessonUrl, { waitUntil: 'load' })
    await page.waitForTimeout(2000);
    await page.screenshot({ path: dir + '/' + 'screenshot2.png' })
/*
// forEach и async не очень дружат
    var videoFrame;
    page.frames().forEach(async (frame) => {
        let tt = await frame.$('#shaka-video');
        if (tt) {
            videoFrame = frame;
        }
    });
*/

    var videoFrame;
    for (const frame of page.frames()) {
        let tt = await frame.$('#shaka-video');
        if (tt) {
            videoFrame = frame;
        }
    }
    if (!videoFrame) {
            throw('#shaka-video not found!');
    }
    if (videoFrame) {
        console.log(videoFrame.url());
        await page.goto(videoFrame.url(), { waitUntil: 'load' })
        await page.waitForTimeout(5000);
        await page.screenshot({ path: dir + '/' + 'screenshot3.png' })
        const filesUrls = await page.evaluate(() => {
            var urls = [];
            var ui0 = document.getElementById('shaka-player').ui;
            urls.push(ui0.b.Fa);
            // "https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.v2.yandex.master.m3u8"
            // ссылка после редиректа получаем подписанные (pre-signed) URL
            // ui0.b.s.K
            // "https://storage-lb.services/hls.webinar/198a2db0-e74b-44c8-ac22-7cec61972e8a.long.v2.yandex.master.m3u8?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=DNmtuhzj5w4VFKS-9BUA%2F20201228%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20201228T142459Z&X-Amz-Expires=300&X-Amz-Signature=0a10db545d16abdfd81c988d7f6e488dfc36502aab60456da78afa9f1ed5bac8&X-Amz-SignedHeaders=host"
            var a0=v0=undefined;
            // select audio and video streams
            ui0.b.s.a.forEach((v,k)=>{ 
                if (!a0 && v.stream.type=='audio') {a0=v;} 
                if (v.stream.type=='video' && (!v0 || v.stream.width > v0.stream.width)) {v0=v;} 
            });
            // load and save selected audio and video segments description files
            urls.push(a0.Wc);
            urls.push(v0.Wc);
//      ["https://storage.svc.services/api/v2/backends/yandex/sets/hls.webinar::198a2db0-e74b-44c8-ac22-7cec61972e8a/objects/long.audio.32kbps.1.1608056896.ts" length: 1 __proto__: Array(0)

            a0.stream.segmentIndex.a.forEach((v,k)=>{ 
                urls = urls.concat(v.c());
            });
            v0.stream.segmentIndex.a.forEach((v,k)=>{ 
                urls = urls.concat(v.c());
            });

            return urls;
        });        
        console.log(filesUrls[0], filesUrls[1], filesUrls[2], filesUrls[3], filesUrls.length);

        var filesUrlsSaved = 0;

        await page.exposeFunction('writefile', async (filePath, data) => {
//          let dataBuffer = new TextEncoder().encode(data);
          filesUrlsSaved++;
          let dataBuffer = new Uint8Array(JSON.parse(data));

          return new Promise((resolve, reject) => {
            fs.writeFile(filePath, dataBuffer, {encoding: null}, (err) => {
              if (err)
                reject(err);
              else
                resolve();
            });
          });
        });

        if (filesUrls.length) {
            fs.writeFileSync(dirOriginal + '/loading', filesUrls[1] + '\n' + filesUrls[2] + '\n', {flag: 'w'});
        }

        for (var loadFile of filesUrls) {
            await page.evaluate(async (dirOriginal, loadFile) => {
                let fileName = loadFile.match('[^/]*$')[0].match('^[^#?]*');
                await fetch(loadFile, {
                    method: "GET",
                    redirect: 'follow',
                }).then((response) => {
                    console.log('url with auth', response.url);
                    return response.arrayBuffer();
                }).then(async function(data) {
//                    let dataView = new Uint8Array(data);
//                    let dataString = new TextDecoder().decode(data);
                    let dataString = '[' + (new Uint8Array(data)).toString() + ']';
                    await window.writefile(dirOriginal + '/' + fileName, dataString);
                });
            }, dirOriginal, loadFile);

/*
            // error 403
           var viewSource = await page.goto(loadFile);
           fs.writeFile(dirOriginal + '/' + fileName, await viewSource.buffer(), function (err) {
               if (err) {
                   return console.log(err);
               }
           });
*/
        }

//        if (filesUrls.length) {
//            fs.writeFileSync(dirOriginal + '/loaded', '', {flag: 'w'});
//        }

        if (filesUrls.length != filesUrlsSaved) {
            console.error('filesUrls:', filesUrls.length, filesUrlsSaved);
            throw('Not all files saved!');
        }

    }

    browser.close()
    console.log('See screenshot in: ' + dir)
  } catch (err) {
    console.error(err);
    browser.close()
  }
})();
Еще чуток бэкенда на php
<?php
...
    // выполнение внешней программы и получение stdout, stderr 
    function exec_return($cmd, $input = '') {
        $descriptorspec = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
        $proc = proc_open($cmd . "\n", $descriptorspec, $pipes);
        foreach ($pipes as $pipe) {
            stream_set_blocking($pipe, false);
        }
        fwrite($pipes[0], $input); fclose($pipes[0]);
//        $stdout = stream_get_contents($pipes[1]); 
//        $stderr = stream_get_contents($pipes[2]); 
        $stdout = ''; 
        $stderr = ''; 
        while (!feof($pipes[1])) $stdout .= fgets($pipes[1],1024);
        while (!feof($pipes[2])) $stderr .= fgets($pipes[2],1024);
        fclose($pipes[1]);
        fclose($pipes[2]);
        $return = proc_close($proc);
        return [
            'stdout' => $stdout,
            'stderr' => $stderr,
            'return' => $return
        ];
    }
...
                    // копируем все ts в один файл output.mp4 без преобразования, очень быстро
                    // при склейке обычно "убегает" звук, "-itsoffset 0.5", сдвигает видео дорожку на 500 мс
                    $command = sprintf('/usr/bin/ffmpeg -loglevel repeat+level+error -nostats -y -i "%s" -itsoffset 0.147978 -i "%s" -vcodec copy -c:a copy "output.mp4"', $m3u8files[0], $m3u8files[1]);
                    $ret = exec_return($command);
                    file_put_contents("{$forConvert->full}/converted/error", $ret['stderr']);
                    chmod("{$forConvert->full}/converted/error", 0666);                        
                    file_put_contents("{$forConvert->full}/converted/converted", $ret['stdout']);
                    chmod("{$forConvert->full}/converted/converted", 0666);

...

Generating Random Number In Each Row

Случайное число в каждой строке выборки.
Проблемка с SQL Server, в каждой строке выборки одно и то же значение, у PostgreSQL, MySQL, Oracle, SQLite всё нормально.
Выкручиваемся создав представление и функцию.
create view m_vw_randView
as
select rand() as random_number
go

create function getRand()
returns float
as
begin
    declare @returnValue float
	select @returnValue = random_number from m_vw_randView
    return @returnValue
end
go

select rand() [rand], dbo.getRand() [getRand], * from (values(1),(2),(3))t(c1)

rand			getRand			c1
0,63453611784667	0,96496883196861	1
0,63453611784667	0,0616408952549143	2
0,63453611784667	0,280773494594934	3
PostgreSQL v12
select *, random() from generate_series(1,3) t1(c1);
| c1  | random              |
| --- | ------------------- |
| 1   | 0.28302426362094124 |
| 2   | 0.38171190130398003 |
| 3   | 0.35524341401745474 |
MySQL v8.0
with recursive t1(c1) AS
(
  select 1
  union all
  select c1 + 1 from t1 where c1 + 1 <= 3
)
select *, rand() from t1;
| c1  | rand()             |
| --- | ------------------ |
| 1   | 0.8239167740791261 |
| 2   | 0.6890084265628909 |
| 3   | 0.9732918413107208 |
Oracle Database 11g
select level c1, dbms_random.value(0,1) random from dual connect by level <= 3
C1	RANDOM
1	0,457075791676919
2	0,885762460182671
3	0,555152770728942

C++ include SQLite


1. Navigate to https://www.sqlite.org/download.html and download latest `amalgamation` source version of SQLite.
2. Extract all the files into your project directory, or your include path, or separate path that you will add/added as include path in your project properties.
3. Run `Developer Command Prompt for VS ****` which is usually available at `Start -> Programs -> Visual Studio **** -> Visual Studio Tools`.
4. Navigate with command prompt to that directory where we extracted our SQLite.
5. Run next command to compile: `cl /c /EHsc sqlite3.c`
6. Run next command to create static library: `lib sqlite3.obj`
7. Open properties of your project and add `sqlite3.lib` to `Linker -> Input -> Additional Dependencies`.
Now you ready to include and use `sqlite3.h` in your project.

Простой сервис родительского контроля (simple service parental control)

По просьбе трудящихся наваял небольшой сервис, выполняет выход из сеанса пользователя (log off) или его отключение (disconnect) в определенное время с временной блокировкой учетной записи, если необходимо. Периодичность проверки попадания в график 1 минута.

Действия по блокировке или отключению учетки применяются только если сессия активна. Учетной запись разблокируется при условиях: учетная запись присутствует в графике, она заблокирована, и в этот момент не попадает в график по отключению. (раз в минуту)

Версия 1.3 — добавлена возможность отключения записи графика. У отключенных текст подкрашен серым.

UPD 09.10.2020 Версия 1.4 — добавлена группировка записей графика и множественное выделение и выключение/включение записей.

chado.zip

chado

Необходимо настроить график отключений, установить и запустить сервис. Данные графика сохраняются в SQLite. После изменения графика необходим перезапуск сервиса, так как сервисом данные считываются только один раз, при запуске. Сервис и GUI-приложение один и тот же бинарный файл, при установке сервиса приложение не копируется, сервис запускается с места нахождения chado.exe

Для disconnect и log off используются WTSDisconnectSession и WTSLogoffSession


for (auto& user : theApp.DBUsers)
{
	if (user.userName.Compare(userName) == 0 
		&& user.dayOfWeek == dayOfWeek
		&& curentSeconds >= user.allowedFrom
		&& curentSeconds <= user.allowedTo
		) {
		if (user.disconnect == 0) {
			WTSDisconnectSession(WTS_CURRENT_SERVER_HANDLE, pInfos[i].SessionId, FALSE);
			theApp.log.WriteString((date.Format(L"%Y-%m-%d %T") + L" disconnect: " + user.userName + L"\r\n").GetString());
		}
		else {
			WTSLogoffSession(WTS_CURRENT_SERVER_HANDLE, pInfos[i].SessionId, FALSE);
			theApp.log.WriteString((date.Format(L"%Y-%m-%d %T") + L" logoff: " + user.userName + L"\r\n").GetString());
		}
		if (user.lock == 0) {
			theApp.unlockAccount(user.userName.GetString(), 1);
		}
	}
}

Для блокировки/разблокировки учетной записи: NetUserGetInfo и NetUserSetInfo


	if (!lock && (ui4->usri4_flags & UF_ACCOUNTDISABLE) == UF_ACCOUNTDISABLE) {
		ui.usri1008_flags = ui4->usri4_flags ^ UF_ACCOUNTDISABLE;
		need = 2;
	}
	if (lock && (ui4->usri4_flags & UF_ACCOUNTDISABLE) != UF_ACCOUNTDISABLE) {
		ui.usri1008_flags = ui4->usri4_flags | UF_ACCOUNTDISABLE;
		need = 1;
	}

	// Call the NetUserSetInfo function
	//  to disable the account, specifying level 1008.
	if (need) {
		nStatus = NetUserSetInfo(NULL, username, 1008, (LPBYTE)&ui, NULL);
		// Display the result of the call.
		if (nStatus == NERR_Success) {
#ifdef _DEBUG
			theApp.log.WriteString((date.Format(L"%Y-%m-%d %T") + (need == 1 ? L" lock " : L" unlock ") + L"account: " + username + L"\r\n").GetString());
#endif
		}
		else {
			theApp.log.WriteString((date.Format(L"%Y-%m-%d %T") + L" error lock/unlock account: " + username + L"\r\n").GetString());
		}
	}

Латеральное объединение (LATERAL JOIN)

Ну вот, хоть в чем-то MySQL переплюнул MariaDB и это LATERAL (на дворе осень 2020)


create table t1(id int, val int);
create table t2(id int, val int);

insert into t1(id, val)
with recursive
r1 as (
  select 1 id, (rand()*100 div 1) r 
  union all
  select r1.id+1 id, (rand()*100 div 1) r from r1 
   where r1.id<100
 ) 
  select id, r from r1
  ;
  
insert into t2(id, val)
with recursive
r1 as (
  select 1 id, (rand()*100 div 1) r 
  union all
  select r1.id+1 id, (rand()*100 div 1) r from r1 
   where r1.id<100
 ) 
  select id, r from r1 where r between 40 and 60
 ;  
 
 select t1.*, t22.* from t1
  join lateral (
  select max(t2.id) ma, min(t2.id) mi from t2 where t1.val between t2.val-10 and t2.val+10
 ) t22 on (true) -- по сути своей left join ибо "on (true)"
-- ) t22 on (t22.ma is not null) -- как костыль inner join можно получить так
 ;

SQL суммы или минимумы-максимумы внутри однотипных повторяющихся подгрупп

SQL суммы или минимумы-максимумы внутри однотипных повторяющихся групп разделяемых другими группами

Или в моем практическом случае была задача на «схлопывание» однотипных диапазонов по времени, минимум и максимум в подгруппе.


**Schema (PostgreSQL v11)**
    
    create table transactions (id int, type int, sum decimal);
    
    insert into transactions
    (id, type, sum)
    VALUES
    (1, 1, 100),(2, 1, 101),
    (3, 2, 200),(4, 2, 201),
    (5, 3, 300),(6, 3, 301),
    (7, 3, 302),(8, 2, 500),
    (9, 2, 510),(10, 1, 20);
        
---
**Query #1**

    with 
    t1 as (
    	select *, lead(type) over (order by id) rn1, lag(type) over (order by id) rn2 from transactions
    ) 
    ,t2 as (
      select t1.*, 
        sum(case when type=rn1 and (rn1!=rn2 or rn2 is null) or (rn1 is null and type!=rn2) then 1 else 0 end) over(order by id) sg
        from t1
     )
    select min(id) first_id_in_group, type, sum(t2.sum) from t2
     group by sg, type
     order by 1;

| first_id_in_group | type | sum  |
| ----------------- | ---- | ---- |
| 1                 | 1    | 201  |
| 3                 | 2    | 401  |
| 5                 | 3    | 903  |
| 8                 | 2    | 1010 |
| 10                | 1    | 20   |
---

С помощью условий находим начала интервалов, обозначаем их 1, последующие записи в группе 0

case when type=rn1 and (rn1!=rn2 or rn2 is null) or (rn1 is null and type!=rn2) then 1 else 0 end

Кумулятивно суммируем и получаем номера групп.

sum() over(order by id)

Ну а дальше всё как обычно.