i, r0b0t - upl04d
6c6966652e75706c6f6164 now is hex
Publicar automaticamente em uma conta do Tiktok;
Num primeiro momento, o upload iria ser feito usando o Puppeteer mas encontrei um repositório que conseguiu fazer o upload usando python
Quando fazemos o login na conta do Tiktok pelo navegador, conseguimos capturar um cookie de sessão chamado sessionid. A partir dele, é possível requisitar rotas do tiktok onde teremos acesso à tokens da AWS que o Tiktok utiliza para realizar as Solicitação do Signature Version 4
Em outras palavras, não vamos precisar simular um usuário carregando um vídeo pelo navegador. Vamos pular essa etapa e fazer o que o site do Tiktok faz quando clicamos no botão de upload.
Para fazer o upload usando o AWS4, serão feitos os seguintes passos, lembrando que todas essas rotas só respondem corretamente usando o cookie de sessão válido:
- Capturar o id do usuário
- Capturar tokens válidos para fazer as assinaturas da AWS
- Gerar a assinatura da AWS4
- Gerar um payload válido no tiktok para o vídeo que vamos enviar
- Gerar um id para o upload que será feito
- Quebrar em pedaços o vídeo e enviar para uma api do Tiktok que realiza checksums desses mini arquivos
- Enviar um comando de "commit" para uma api do Tiktok, confirmando o upload dos checksums
- Por fim, criar uma publicação na conta do Tiktok vinculando com oo id do vídeo que foi enviado.
Capturar o id do usuário
Aqui vamos capturar apenas o user_id para usar mais pra frente.
Capturar tokens válidos para fazer as assinaturas da AWS
Do payload de resposta, nós vamos usar alguns valores da chave video_token_v5, sendo eles os dados necessários para criarmos a assinatura da AWS4.
- access_key_id
- secret_acess_key
- session_token
Notou algo de diferente? Pois é, não é só nós que escrevemos nomes em inglês errado, a galera do tiktok também confunde acess com access. Fiquei pensando o motivo deles não corrigirem o nome, provavelmente existem muitos lugares para alterar pensando no ecossistema completo do projeto.
Gerar a assinatura da AWS4
Aqui o bicho pegou, tive que dedicar um tempinho pra entender como que isso funciona e tentar escrever de uma forma mais simples. Essa assinatura significa mais ou menos assim: Eu possuo uma querystring que descreve as ações que quero fazer em determinada API, fazemos alguns ajustes nessa querystring de modo que conseguimos montar uma pseudo URL e depois é feita uma assinatura sha256 nela.
A função principal que faz a assinatura ficou assim:
Restante das Funções de Assinatura
import crypto from "crypto"
import { UploadParameters } from "./retrieves"
import { hash, hmac } from "../utils/hash"
const ALGORITHM = "AWS4-HMAC-SHA256"
const AWS_REGION = "us-east-1"
const AWS_SERVICE = "vod"
export const makeAwsSignature = (uploadParameters: UploadParameters, requestParameters: string, headers: Record<string, string>, method = "GET", payload = "") => {
const amzdate = headers["x-amz-date"]
const datestamp = amzdate.split("T")[0]
const stringToSign = makeStringToSign(requestParameters, headers, method, payload)
const sigingKey = getSignatureKey(uploadParameters.tokens.secret_key, datestamp, AWS_REGION, AWS_SERVICE)
const signature = hmac(sigingKey, stringToSign, "hex")
return signature
}
As funções de hash e hmac:
import crypto from "crypto"
export const hmac = (key, string, encoding) => {
return crypto.createHmac("sha256", key).update(string, "utf8").digest(encoding)
}
export const hash = (string, encoding) => {
return crypto.createHash("sha256").update(string, "utf8").digest(encoding)
}
Gerar um payload válido no tiktok para o vídeo que vamos enviar
URL: https://vod-us-east-1.bytevcloudapi.com/
Com a assinatura em mãos, podemos enviar uma requisição para obter um payload válido conténdo id se chaves que serão usadas no upload do vído:
const buildUploadData = async (videopath: string, uploadParameters: UploadParameters): Promise<UploadData> => {
info("creating aws4 signatures")
const videoFileSize = await sizeOfFile(videopath)
const requestParameters = `Action=ApplyUploadInner&FileSize=${videoFileSize}&FileType=video&IsInner=1&SpaceName=tiktok&Version=2020-11-19&s=zdxefu8qvq8`
const date = tiktokAmzDate()
const headers = {
"x-amz-date": date.datetime,
"x-amz-security-token": uploadParameters.tokens.session_token,
}
const signature = makeAwsSignature(uploadParameters, requestParameters, headers)
const authorization = `AWS4-HMAC-SHA256 Credential=${uploadParameters.tokens.access_key}/${date.datestamp}/us-east-1/vod/aws4_request, SignedHeaders=x-amz-date;x-amz-security-token, Signature=${signature}`
headers["authorization"] = authorization
const response = await fetch(`${VOD_URL}?${requestParameters}`, { headers })
const responseData = await response.json()
const uploadNode = responseData["Result"]["InnerUploadAddress"]["UploadNodes"][0]
success("creating aws4 signatures")
return {
video_id: uploadNode["Vid"],
store_uri: uploadNode["StoreInfos"][0]["StoreUri"],
video_auth: uploadNode["StoreInfos"][0]["Auth"],
upload_host: uploadNode["UploadHost"],
session_key: uploadNode["SessionKey"],
}
}
Gerar um id para o upload que será feito
Aqui o processo é simples, basicamente enviamos um POST pra uma rota passando um dígito no form-data e com o header de Authorization preenchido com o video_auth que capturamos no passo anterior. A resposta é um ID.
const getUploadId = async (request: AxiosInstance, uploadData) => {
info("generating upload Id")
const uploadUrl = `https://${uploadData.upload_host}/${uploadData.store_uri}?uploads`
const randomDigit = randomInt(0, 9)
const headers = {
Authorization: uploadData.video_auth,
"Content-Type": `multipart/form-data; boundary=---------------------------${randomDigit}`,
}
const data = `-----------------------------${randomDigit}--`
const response = await request.post(uploadUrl, data, {
headers: headers,
})
const responseData = response.data
success("generating upload Id")
return responseData.payload.uploadID
}
Quebrar em pedaços o vídeo e enviar para uma api do Tiktok que realiza checksums desses mini arquivos
O processo feito aqui consiste em quebrarmos em pedaços os bytes do arquivo MP4, enviar todos para o Tiktok enquanto geramos hashs do tipo CRC32, esse algoritmo é usado para detectar alterações entre informações, para depois realizar o famoso checksum.
const uploadFileChunks = async (request: AxiosInstance, uploadData, videoPath: string, uploadId: string) => {
info("uploading file chunks")
const fileContent = await fs.readFileSync(videoPath)
const fileLength = fileContent.length
const chunkSize = 5242880
const chunks = []
const crcs = []
let i = 0
while (i < fileLength) {
chunks.push(fileContent.slice(i, i + chunkSize))
i += chunkSize
}
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i]
const crc = crcCalc(chunk)
crcs.push(crc)
const url = `https://${uploadData.upload_host}/${uploadData.store_uri}?partNumber=${i + 1}&uploadID=${uploadId}`
const headers = {
Authorization: uploadData.video_auth,
"Content-Type": "application/octet-stream",
"Content-Disposition": 'attachment; filename="undefined"',
"Content-Crc32": crc,
}
const response = await request.post(url, chunk, {
method: "POST",
headers: headers,
})
}
success("uploading file chunks")
return crcs
}
Notem que aqui foi utilizado o uploadId gerado no passo anterior. Notem também que é retornado o array de CRCs para ser validado depois.
Checksum
O checksum é feito enviando um POST para uma API passando no corpo da requisição uma string com todos os CRCs gerados no upload das partes.
const uploadFileChecksums = async (request: AxiosInstance, uploadData, uploadId: string, crcs) => {
info("uploading file checksums")
const url = `https://${uploadData.upload_host}/${uploadData.store_uri}?uploadID=${uploadId}`
const headers = {
Authorization: uploadData.video_auth,
"Content-Type": "text/plain;charset=UTF-8",
}
const data = crcs.map((crc, i) => `${i + 1}:${crc}`).join(",")
const response = await request.post(url, data, {
headers: headers,
})
success("uploading file checksums")
}
Commitar o upload
URL: https://vod-us-east-1.bytevcloudapi.com/
Nesse ponto também precisamos criar uma assinatura AWS4 para ser enviado o comando de commit. Provavelmente nesse ponto a API do Tiktok confirma que o Arquivo X realmente será enviado e passa a ter alguma flag onde o Tiktok possa bloquear que sejam enviados vídeos com o mesmo conteúdo. Afinal, o checksum nesse cenário parece muito com o cenário onde os anti-virus criam checksums para detectar vírus no computador
const commitUpload = async (request: AxiosInstance, uploadData: UploadData, uploadParameters: UploadParameters) => {
info("committing video upload to aws")
const requestParameters = "Action=CommitUploadInner&SpaceName=tiktok&Version=2020-11-19"
const date = tiktokAmzDate()
const data = `{"SessionKey":"${uploadData.session_key}","Functions":[]}`
const amzContentSha256 = hash(data, "hex")
const headers = {
"x-amz-content-sha256": amzContentSha256,
"x-amz-date": date.datetime,
"x-amz-security-token": uploadParameters.tokens.session_token,
}
const signature = makeAwsSignature(uploadParameters, requestParameters, headers, "POST", data)
const authorization = `AWS4-HMAC-SHA256 Credential=${uploadParameters.tokens.access_key}/${date.datestamp}/us-east-1/vod/aws4_request, SignedHeaders=x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=${signature}`
headers["authorization"] = authorization
headers["Content-Type"] = "text/plain;charset=UTF-8"
const response = await request.post(`${VOD_URL}?${requestParameters}`, data, {
headers: headers,
})
success("committing video upload to aws")
}
Depois de commitar e não ocorrer nenhum problema, já é possível criar a publicação no Tiktok.
Criar a publicação no Tiktok
URL: https://www.tiktok.com/api/v1/item/create/
O upload é muito simples, precisamos passar por uma API do Tiktok que checa se as hashtags são válidas (para poder gerar o redirecionamento quando o usuário clicar nelas) e depois captuar token CSRF.
const getCSRFToken = async (request: AxiosInstance) => {
const createUrl = CREATE_VIDEO_URL
const headers = {
"X-Secsdk-Csrf-Request": "1",
"X-Secsdk-Csrf-Version": "1.2.8",
}
const response = await request(createUrl, { method: "HEAD", headers: headers })
return response.headers["x-ware-csrf-token"].split(",")[1]
}
const verifyVideoHashtags = async (request: AxiosInstance, title: string, tags: string[]): Promise<VerifiedTags> => {
info("verifying post hashtags")
let text = title
let textExtra = []
for (const tag of tags) {
const url = "https://www.tiktok.com/api/upload/challenge/sug/"
const params = { keyword: tag }
const queryString = querystring.stringify(params)
const response = await request(`${url}?${queryString}`)
let verifiedTag
try {
const responseData = response.data
verifiedTag = responseData.sug_list[0].cha_name
} catch (error) {
verifiedTag = tag
}
text += ` #${verifiedTag}`
textExtra.push({
start: text.length - verifiedTag.length - 1,
end: text.length,
user_id: "",
type: 1,
hashtag_name: verifiedTag,
})
}
success("verifying post hashtags")
return { text, text_extra: textExtra }
}
Depois disso podemos enviar um POST para a rota de criação de publicações com alguns parâmetros que foram gerados ao longo do caminho. Eu apanhei aqui pois fiquei por um bom tempo tentando enviar o POST passando os parâmetros como um JSON, mas na real você precisa enviar um POST com uma querystring kkkkk
const createTiktokVideo = async (request: AxiosInstance, title: string, tags: string[], uploadData: UploadData) => {
info("finalizing upload to tiktok")
const { text, text_extra } = await verifyVideoHashtags(request, title, tags)
const csrfToken = await getCSRFToken(request)
const headers = {
"X-Secsdk-Csrf-Token": csrfToken,
}
const params = JSON.stringify({
video_id: uploadData.video_id,
visibility_type: "0",
poster_delay: "0",
text: text,
text_extra: JSON.stringify(text_extra),
allow_comment: "1",
allow_duet: "0",
allow_stitch: "0",
sound_exemption: "0",
aid: "1988",
})
const response = await request.post("https://www.tiktok.com/api/v1/item/create/?" + querystring.stringify(JSON.parse(params)), {}, { headers })
if (response.data.status_code !== 0) {
throw new Error()
}
success("finalizing upload to tiktok")
}
Feito isso essa API fará a vinculação do ID do vídeo que subimos, com as hashtags que validamos e irá criar uma publicação no perfil que possui o cookie sessionid usado nessas requisições.
Um detalhe, em todas as requisições utilizei uma mesma instância do axios onde configurei o token se sessão na criação dele. Assim toda request terá o cookie. Configurei também o CookieJar para armazenar nas próximas requisições todo cookie que as APIs do Tiktok devolve, simulando assim uma integração pelo navegador.
import axios from "axios"
import tough from "tough-cookie"
import { wrapper } from "axios-cookiejar-support"
const cookieJar = new tough.CookieJar()
wrapper(axios)
export const createRequestInstance = (sessionId: string) => {
return axios.create({
withCredentials: true,
headers: { Cookie: `sessionid=${sessionId}; domain=.tiktok.com` },
jar: cookieJar,
})
}
Algo que chamou minha atenção foi essa autenticação AWS4. Já vi alguns cenários onde ela é usada para acessar informações de S3, EC2, etc ... Talvez eu anime e faça alguns testes com essas informações.