Home🏡

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.

Post Principal