Compare commits
4 Commits
dev
...
878ee963b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
878ee963b5 | ||
|
|
c955ee08d4 | ||
|
|
08cc4f9d75 | ||
|
|
9ba4be8a92 |
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
258
README.md
258
README.md
@@ -1,121 +1,193 @@
|
|||||||
# Neocities C CLI
|
# Neocities C API
|
||||||
|
|
||||||
A simple command-line tool written in C that interacts with the [Neocities API](https://neocities.org/api).
|
Minimal, secure C wrapper for the Neocities API using API key authentication.
|
||||||
Currently, only the `--info` command is implemented — it fetches and displays basic information about your Neocities site.
|
|
||||||
|
No login flow.
|
||||||
|
No credential storage.
|
||||||
|
Stateless by design.
|
||||||
|
|
||||||
|
Users must generate their own API key via the Neocities dashboard.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Fetch site information using Neocities API
|
|
||||||
- Authenticates via environment variables
|
* Site info retrieval
|
||||||
- JSON parsing with [jansson](https://digip.org/jansson/)
|
* File listing
|
||||||
- Works on Linux, Windows (MSYS2/MinGW), and Android (via Termux)
|
* Upload files
|
||||||
- Compilable with a Makefile for easy builds
|
* Delete files
|
||||||
|
* TLS verification enabled by default
|
||||||
|
|
||||||
|
Authentication is performed exclusively via:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <API_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You’ll need:
|
* libcurl
|
||||||
- A C compiler (`gcc` or `clang`)
|
|
||||||
- [libcurl](https://curl.se/libcurl/)
|
|
||||||
- [jansson](https://digip.org/jansson/)
|
|
||||||
- `make`
|
|
||||||
|
|
||||||
### Linux (Debian/Ubuntu-based)
|
Linux (example):
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install build-essential libcurl4-openssl-dev libjansson-dev make
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows (MSYS2 / MinGW)
|
|
||||||
```bash
|
|
||||||
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-curl mingw-w64-x86_64-jansson make
|
|
||||||
```
|
|
||||||
|
|
||||||
### Android (Termux)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pkg install clang curl-dev jansson make
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Run in cloned repository:
|
|
||||||
```bash
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
This will generate the executable neocities (or neocities.exe on Windows).
|
|
||||||
|
|
||||||
To clean build files:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make clean
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Before running, export your Neocities credentials:
|
|
||||||
|
|
||||||
#### Linux / macOS / Termux
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export NEOCITIES_USER="your_username"
|
|
||||||
export NEOCITIES_PASS="your_password"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Windows (PowerShell)
|
|
||||||
|
|
||||||
```batch
|
|
||||||
setx NEOCITIES_USER "your_username"
|
|
||||||
setx NEOCITIES_PASS "your_password"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./neocities --info
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Neocities C CLI
|
sudo apt install libcurl4-openssl-dev
|
||||||
|
|
||||||
Sitename: example
|
|
||||||
Hits: 42
|
|
||||||
Created at: 2024-01-10
|
|
||||||
Last updated: 2025-11-01
|
|
||||||
Domain: domain.com
|
|
||||||
Tags: art, blog, personal.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional: Install Globally
|
---
|
||||||
|
|
||||||
You can move the compiled executable to a directory in your PATH to run it from anywhere:
|
## Authentication
|
||||||
|
|
||||||
#### Linux / Termux
|
Generate your API key at:
|
||||||
|
|
||||||
```bash
|
Neocities > Settings > Manage Site Settings > API
|
||||||
sudo mv neocities /usr/local/bin/
|
(or https://neocities.org/settings/YOUR-USERNAME#api_key)
|
||||||
|
|
||||||
|
If you're coding a CLI or application, you must provide the key at runtime (env var, config file, etc).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Overview
|
||||||
|
|
||||||
|
Just an overview. For more detailed information, see: [[Function-Guide.md]]
|
||||||
|
|
||||||
|
### Info (public)
|
||||||
|
|
||||||
|
```c
|
||||||
|
int neocities_info(const char *sitename, neocities_info_t *out);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Windows (PowerShell)
|
Fetch metadata about a site.
|
||||||
Move neocities.exe to a folder in your PATH, e.g., C:\Windows\System32\.
|
|
||||||
|
|
||||||
After that, you can run:
|
---
|
||||||
|
|
||||||
```batch
|
### List files
|
||||||
neocities --info
|
|
||||||
|
```c
|
||||||
|
int neocities_list(const char *api_key,
|
||||||
|
const char *path,
|
||||||
|
neocities_filelist_t *out);
|
||||||
```
|
```
|
||||||
|
|
||||||
from any folder without specifying the path.
|
List files at a path.
|
||||||
|
|
||||||
## Notes
|
---
|
||||||
|
|
||||||
Only the --info command is implemented for now.
|
### Upload
|
||||||
|
|
||||||
If you get curl_easy_perform errors, check your network and SSL setup.
|
```c
|
||||||
|
int neocities_upload(const char *api_key,
|
||||||
|
const char **local_files,
|
||||||
|
const char **remote_names,
|
||||||
|
size_t count,
|
||||||
|
char **response);
|
||||||
|
```
|
||||||
|
|
||||||
|
Uploads multiple files.
|
||||||
|
|
||||||
|
* `local_files[i]` → local path
|
||||||
|
* `remote_names[i]` → remote filename
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
```c
|
||||||
|
int neocities_delete(const char *api_key,
|
||||||
|
const char **filenames,
|
||||||
|
size_t count,
|
||||||
|
char **response);
|
||||||
|
```
|
||||||
|
|
||||||
|
Deletes files from the site.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### API Key
|
||||||
|
|
||||||
|
For security reasons, you should get the API key via Neocities settings. This function is not avaiable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
Always free parsed outputs:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void neocities_free_info(neocities_info_t *info);
|
||||||
|
void neocities_free_filelist(neocities_filelist_t *list);
|
||||||
|
```
|
||||||
|
|
||||||
|
`response` returned by upload/delete must also be freed by caller.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### List files
|
||||||
|
|
||||||
|
```c
|
||||||
|
neocities_filelist_t list;
|
||||||
|
|
||||||
|
if (neocities_list(api_key, "/", &list) == 0) {
|
||||||
|
for (size_t i = 0; i < list.count; i++)
|
||||||
|
printf("%s\n", list.paths[i]);
|
||||||
|
|
||||||
|
neocities_free_filelist(&list);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Upload
|
||||||
|
|
||||||
|
```c
|
||||||
|
const char *local[] = {"index.html"};
|
||||||
|
const char *remote[] = {"index.html"};
|
||||||
|
char *resp;
|
||||||
|
|
||||||
|
neocities_upload(api_key, local, remote, 1, &resp);
|
||||||
|
free(resp);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
* TLS verification is enforced
|
||||||
|
* Redirects are disabled intentionally
|
||||||
|
* API key is never persisted internally
|
||||||
|
|
||||||
|
Recommended:
|
||||||
|
|
||||||
|
* Store key in env vars
|
||||||
|
* Avoid hardcoding
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
* No login/password support
|
||||||
|
* API key only
|
||||||
|
* Explicit memory ownership
|
||||||
|
* Minimal abstraction
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Extensions (Optional)
|
||||||
|
|
||||||
|
Possible areas to explore:
|
||||||
|
|
||||||
|
* Structured responses for upload/delete
|
||||||
|
* JSON passthrough mode
|
||||||
|
* Streaming uploads
|
||||||
|
* Error codes standardization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
On Termux, use clang for the best compatibility.
|
|
||||||
156
src/info.c
156
src/info.c
@@ -1,156 +0,0 @@
|
|||||||
#include "main.h" // Header local do projeto (declarações próprias: struct response, write_callback, etc.)
|
|
||||||
#include <stdio.h> // printf, fprintf
|
|
||||||
#include <stdlib.h> // getenv, malloc, free
|
|
||||||
#include <curl/curl.h> // libcurl: HTTP/HTTPS client
|
|
||||||
#include <jansson.h> // Jansson: parsing e manipulação de JSON
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
// Função responsável por buscar informações do site do usuário no Neocities
|
|
||||||
int fetch_neocities_info() {
|
|
||||||
|
|
||||||
// Lê variáveis de ambiente com credenciais
|
|
||||||
// NEOCITIES_USER e NEOCITIES_PASS são usadas para autenticação HTTP Basic
|
|
||||||
const char *user = getenv("NEOCITIES_USER");
|
|
||||||
const char *pass = getenv("NEOCITIES_PASS");
|
|
||||||
// const char *enc = getenv("NEOCITIES_PASS_ENC"); // Não usado aqui (possível uso futuro)
|
|
||||||
|
|
||||||
// Verificação básica: se usuário ou senha não existirem, aborta
|
|
||||||
if (!user || !pass) {
|
|
||||||
fprintf(stderr, "You're not logged!\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buffer para "user:password", formato exigido por CURLOPT_USERPWD
|
|
||||||
char auth[256];
|
|
||||||
snprintf(auth, sizeof(auth), "%s:%s", user, pass);
|
|
||||||
|
|
||||||
// Inicializa um handle do libcurl
|
|
||||||
CURL *curl = curl_easy_init();
|
|
||||||
if (!curl) {
|
|
||||||
fprintf(stderr, "Erro ao inicializar cURL\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monta a URL da API do Neocities para obter informações do site
|
|
||||||
char infourl[256];
|
|
||||||
snprintf(
|
|
||||||
infourl,
|
|
||||||
sizeof(infourl),
|
|
||||||
"https://neocities.org/api/info?sitename=%s",
|
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define a URL da requisição
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, infourl);
|
|
||||||
|
|
||||||
// Define autenticação HTTP Basic (user:pass)
|
|
||||||
curl_easy_setopt(curl, CURLOPT_USERPWD, auth);
|
|
||||||
|
|
||||||
// Estrutura usada para armazenar a resposta HTTP em memória
|
|
||||||
// resp.data será expandido pelo write_callback
|
|
||||||
struct response resp = { .data = malloc(1), .len = 0 };
|
|
||||||
resp.data[0] = '\0'; // Garante string vazia inicial
|
|
||||||
|
|
||||||
// Função chamada pelo libcurl sempre que dados são recebidos
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
|
|
||||||
|
|
||||||
// Ponteiro passado para o write_callback (estado da resposta)
|
|
||||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
|
||||||
|
|
||||||
// Executa a requisição HTTP de forma síncrona
|
|
||||||
CURLcode res = curl_easy_perform(curl);
|
|
||||||
if (res != CURLE_OK) {
|
|
||||||
fprintf(stderr, "Erro na requisição: %s\n", curl_easy_strerror(res));
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
free(resp.data);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Libera o handle do curl (a resposta já está em resp.data)
|
|
||||||
curl_easy_cleanup(curl);
|
|
||||||
|
|
||||||
// Estrutura para capturar erros de parsing JSON
|
|
||||||
json_error_t error;
|
|
||||||
|
|
||||||
// Converte a string JSON recebida em um objeto JSON manipulável
|
|
||||||
json_t *obj = json_loads(resp.data, 0, &error);
|
|
||||||
free(resp.data); // Não é mais necessária após o parsing
|
|
||||||
|
|
||||||
// Caso o JSON seja inválido
|
|
||||||
if (!obj) {
|
|
||||||
fprintf(stderr, "Erro ao parsear JSON: %s\n", error.text);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtém o campo "result" do JSON raiz
|
|
||||||
json_t *result_obj = json_object_get(obj, "result");
|
|
||||||
const char *result_str = json_string_value(result_obj);
|
|
||||||
|
|
||||||
// Caso a API tenha retornado erro
|
|
||||||
if (strcmp(result_str, "error") == 0) {
|
|
||||||
|
|
||||||
// Tipo de erro retornado pela API
|
|
||||||
const char *err_type =
|
|
||||||
json_string_value(json_object_get(obj, "error_type"));
|
|
||||||
|
|
||||||
// Mensagem de erro legível
|
|
||||||
const char *err_msg =
|
|
||||||
json_string_value(json_object_get(obj, "message"));
|
|
||||||
|
|
||||||
// Tratamento específico para erro de autenticação
|
|
||||||
if (strcmp(err_type, "invalid_auth") == 0) {
|
|
||||||
printf("Usuário ou senha incorretos!\n");
|
|
||||||
} else {
|
|
||||||
printf("Erro! %s\n", err_msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diminui o contador de referência do objeto JSON
|
|
||||||
json_decref(obj);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso a requisição tenha sido bem-sucedida
|
|
||||||
if (strcmp(result_str, "success") == 0) {
|
|
||||||
|
|
||||||
// Objeto "info" contém os dados do site
|
|
||||||
json_t *info_obj = json_object_get(obj, "info");
|
|
||||||
|
|
||||||
// Impressão direta dos campos retornados
|
|
||||||
printf("\nSitename: %s\n",
|
|
||||||
json_string_value(json_object_get(info_obj, "sitename")));
|
|
||||||
|
|
||||||
printf("Hits: %d\n",
|
|
||||||
(int)json_integer_value(json_object_get(info_obj, "hits")));
|
|
||||||
|
|
||||||
printf("Created at: %s\n",
|
|
||||||
json_string_value(json_object_get(info_obj, "created_at")));
|
|
||||||
|
|
||||||
printf("Last updated: %s\n",
|
|
||||||
json_string_value(json_object_get(info_obj, "last_updated")));
|
|
||||||
|
|
||||||
printf("Domain: %s\n",
|
|
||||||
json_string_value(json_object_get(info_obj, "domain")));
|
|
||||||
|
|
||||||
// Array de tags associadas ao site
|
|
||||||
json_t *tags = json_object_get(info_obj, "tags");
|
|
||||||
|
|
||||||
printf("Tags: ");
|
|
||||||
size_t index;
|
|
||||||
json_t *tag;
|
|
||||||
|
|
||||||
// Itera sobre o array JSON "tags"
|
|
||||||
json_array_foreach(tags, index, tag) {
|
|
||||||
printf(
|
|
||||||
"#%s%s",
|
|
||||||
json_string_value(tag),
|
|
||||||
(index < json_array_size(tags) - 1) ? ", " : "."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
printf("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Libera o objeto JSON principal
|
|
||||||
json_decref(obj);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
12
src/info.h
12
src/info.h
@@ -1,12 +0,0 @@
|
|||||||
#ifndef INFO
|
|
||||||
#define INFO
|
|
||||||
// ^ Guardas de inclusão (include guards)
|
|
||||||
// Evitam que este header seja processado mais de uma vez
|
|
||||||
// durante a compilação, o que causaria redefinições.
|
|
||||||
|
|
||||||
// Declaração da função que busca informações do Neocities.
|
|
||||||
// O `void` indica explicitamente que a função não recebe parâmetros.
|
|
||||||
int fetch_neocities_info(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
// ^ Fim do include guard
|
|
||||||
BIN
src/libneocities.a
Normal file
BIN
src/libneocities.a
Normal file
Binary file not shown.
27
src/login.sh
27
src/login.sh
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
CONFIG_DIR="$HOME/.config/neocities"
|
|
||||||
CONFIG_FILE="$CONFIG_DIR/credentials"
|
|
||||||
|
|
||||||
mkdir -p "$CONFIG_DIR"
|
|
||||||
chmod 700 "$CONFIG_DIR"
|
|
||||||
|
|
||||||
read -p "Username: " USER
|
|
||||||
read -s -p "Password: " PASS
|
|
||||||
echo
|
|
||||||
|
|
||||||
read -p "Would you like to see the credentials on your screen to confirm? [Y/n]: " CHOICE
|
|
||||||
case $CHOICE in
|
|
||||||
y|Y) echo -e "Username: $USER\nPassword: $PASS" ;;
|
|
||||||
*) echo "" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
cat > "$CONFIG_FILE" <<EOF
|
|
||||||
user = $USER
|
|
||||||
pass = $PASS
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 600 "$CONFIG_FILE"
|
|
||||||
|
|
||||||
echo "Credentials saved in $CONFIG_FILE"
|
|
||||||
93
src/main.c
93
src/main.c
@@ -1,93 +0,0 @@
|
|||||||
#include <stdio.h> // printf, fprintf
|
|
||||||
#include <stdlib.h> // malloc, realloc, free
|
|
||||||
#include <string.h> // strcmp, memcpy
|
|
||||||
#include <curl/curl.h> // libcurl (necessário para callbacks e tipos)
|
|
||||||
|
|
||||||
#include "main.h" // Declarações globais do projeto (upload_func, etc.)
|
|
||||||
#include "info.h" // Declaração de fetch_neocities_info()
|
|
||||||
|
|
||||||
// Estrutura usada para acumular a resposta HTTP em memória.
|
|
||||||
// É preenchida incrementalmente pelo write_callback.
|
|
||||||
struct response {
|
|
||||||
char *data; // Buffer dinâmico com os dados recebidos
|
|
||||||
size_t len; // Quantidade atual de bytes válidos em data
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback chamado pelo libcurl sempre que um novo bloco de dados chega.
|
|
||||||
// Não garante tamanho fixo nem chamada única.
|
|
||||||
size_t write_callback(void *data, size_t size, size_t nmemb, void *userdata) {
|
|
||||||
|
|
||||||
// userdata aponta para a struct response passada via CURLOPT_WRITEDATA
|
|
||||||
struct response *resp = (struct response *)userdata;
|
|
||||||
|
|
||||||
// Tamanho real do bloco recebido
|
|
||||||
size_t chunk_size = size * nmemb;
|
|
||||||
|
|
||||||
// Realoca o buffer para caber os novos dados + '\0'
|
|
||||||
char *tmp = realloc(resp->data, resp->len + chunk_size + 1);
|
|
||||||
if (!tmp) {
|
|
||||||
// Se realloc falhar, aborta a escrita
|
|
||||||
// Retornar 0 sinaliza erro ao libcurl
|
|
||||||
fprintf(stderr, "Erro de memória\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atualiza o ponteiro com o novo buffer
|
|
||||||
resp->data = tmp;
|
|
||||||
|
|
||||||
// Copia o bloco recebido para o final do buffer atual
|
|
||||||
memcpy(resp->data + resp->len, data, chunk_size);
|
|
||||||
|
|
||||||
// Atualiza o tamanho total acumulado
|
|
||||||
resp->len += chunk_size;
|
|
||||||
|
|
||||||
// Garante que o buffer seja uma string C válida
|
|
||||||
resp->data[resp->len] = '\0';
|
|
||||||
|
|
||||||
// Retornar o número de bytes processados indica sucesso
|
|
||||||
return chunk_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tipo de ponteiro para função que representa um comando CLI.
|
|
||||||
// Todas as funções de comando retornam int e não recebem argumentos.
|
|
||||||
typedef int (*cmd_func_t)(void);
|
|
||||||
|
|
||||||
// Estrutura que associa:
|
|
||||||
// - nome do comando (string digitada no terminal)
|
|
||||||
// - função que implementa esse comando
|
|
||||||
typedef struct {
|
|
||||||
const char *name;
|
|
||||||
cmd_func_t func;
|
|
||||||
} command_entry;
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
|
|
||||||
// Tabela de comandos suportados pelo programa.
|
|
||||||
// Facilita adicionar novos comandos sem alterar a lógica principal.
|
|
||||||
command_entry commands[] = {
|
|
||||||
{"--info", fetch_neocities_info},
|
|
||||||
// {"--upload", upload_func}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verifica se ao menos um argumento foi passado
|
|
||||||
if (argc < 2) {
|
|
||||||
printf("No command provided.\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Percorre a tabela de comandos procurando correspondência
|
|
||||||
for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) {
|
|
||||||
|
|
||||||
// Compara o argumento passado com o nome do comando
|
|
||||||
if (strcmp(argv[1], commands[i].name) == 0) {
|
|
||||||
|
|
||||||
// Executa a função associada ao comando
|
|
||||||
// e retorna imediatamente o código dela
|
|
||||||
return commands[i].func();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se nenhum comando conhecido foi encontrado
|
|
||||||
printf("Unknown command.\n");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
31
src/main.h
31
src/main.h
@@ -1,31 +0,0 @@
|
|||||||
#ifndef MAIN_H
|
|
||||||
#define MAIN_H
|
|
||||||
// Include guard:
|
|
||||||
// Impede múltiplas inclusões deste header no mesmo processo de compilação.
|
|
||||||
|
|
||||||
#include <stddef.h> // size_t
|
|
||||||
// Necessário para o tipo size_t, usado em tamanhos e contadores de memória.
|
|
||||||
|
|
||||||
// Declarações externas de variáveis globais.
|
|
||||||
// O `extern` indica que a definição real existe em outro arquivo .c.
|
|
||||||
extern const char *user;
|
|
||||||
extern const char *pass;
|
|
||||||
|
|
||||||
// Estrutura usada para armazenar dados recebidos dinamicamente,
|
|
||||||
// geralmente como resposta de uma requisição HTTP.
|
|
||||||
struct response {
|
|
||||||
char *data; // Buffer dinâmico contendo os dados acumulados
|
|
||||||
size_t len; // Quantidade de bytes válidos em data
|
|
||||||
};
|
|
||||||
|
|
||||||
// Protótipo da função de callback usada pelo libcurl.
|
|
||||||
// Essa função será chamada repetidamente conforme dados chegam.
|
|
||||||
size_t write_callback(
|
|
||||||
void *ptr, // Ponteiro para os dados recebidos
|
|
||||||
size_t size, // Tamanho de cada elemento
|
|
||||||
size_t nmemb, // Número de elementos
|
|
||||||
void *userdata // Ponteiro de contexto definido pelo usuário
|
|
||||||
);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
// Fim do include guard
|
|
||||||
321
src/neocities.c
Normal file
321
src/neocities.c
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
#include "neocities.h"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <jansson.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// HTTP responde buffer
|
||||||
|
// -------------------------
|
||||||
|
struct response {
|
||||||
|
char *data;
|
||||||
|
size_t len;
|
||||||
|
};
|
||||||
|
|
||||||
|
static size_t write_cb(
|
||||||
|
void *ptr,
|
||||||
|
size_t size,
|
||||||
|
size_t nmemb,
|
||||||
|
void *userdata
|
||||||
|
)
|
||||||
|
{
|
||||||
|
struct response *r = (struct response*)userdata;
|
||||||
|
size_t chunk = size * nmemb;
|
||||||
|
char *tmp = realloc(r->data, r->len + chunk + 1);
|
||||||
|
if (!tmp) return 0;
|
||||||
|
r->data = tmp;
|
||||||
|
memcpy(r->data + r->len, ptr, chunk);
|
||||||
|
r->len += chunk;
|
||||||
|
r->data[r->len] = '\0';
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// HTTP helpers
|
||||||
|
// -------------------------
|
||||||
|
static int perform_request(
|
||||||
|
const char *url,
|
||||||
|
const char *api_key,
|
||||||
|
const char *post_fields,
|
||||||
|
struct response *resp,
|
||||||
|
curl_mime *mime
|
||||||
|
)
|
||||||
|
{
|
||||||
|
CURL *curl = curl_easy_init();
|
||||||
|
if (!curl) return 1;
|
||||||
|
|
||||||
|
resp->data = malloc(1);
|
||||||
|
resp->len = 0;
|
||||||
|
resp->data[0] = '\0';
|
||||||
|
|
||||||
|
struct curl_slist *headers = NULL;
|
||||||
|
|
||||||
|
if (api_key && strlen(api_key) > 0) {
|
||||||
|
char auth_header[512];
|
||||||
|
snprintf(auth_header, sizeof(auth_header),
|
||||||
|
"Authorization: Bearer %s", api_key);
|
||||||
|
headers = curl_slist_append(headers, auth_header);
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||||
|
|
||||||
|
if (headers)
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
|
||||||
|
/* SSL hardening */
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L); // política
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, resp);
|
||||||
|
|
||||||
|
if (post_fields) {
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mime)
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
long http_code = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
|
||||||
|
if (res != CURLE_OK || http_code >= 400) {
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
free(resp->data);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// JSON parsers
|
||||||
|
// -------------------------
|
||||||
|
void neocities_free_info(
|
||||||
|
neocities_info_t *info
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!info) return;
|
||||||
|
free(info->sitename);
|
||||||
|
free(info->created_at);
|
||||||
|
free(info->last_updated);
|
||||||
|
free(info->domain);
|
||||||
|
for (size_t i=0; i<info->tag_count; i++) free(info->tags[i]);
|
||||||
|
free(info->tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void neocities_free_filelist(
|
||||||
|
neocities_filelist_t *list
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!list) return;
|
||||||
|
for (size_t i=0; i<list->count; i++) free(list->paths[i]);
|
||||||
|
free(list->paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// info — GET /api/info
|
||||||
|
// -------------------------
|
||||||
|
int neocities_info(
|
||||||
|
const char *sitename, neocities_info_t *out
|
||||||
|
)
|
||||||
|
{
|
||||||
|
char url[512];
|
||||||
|
if (sitename)
|
||||||
|
snprintf(url, sizeof(url), "https://neocities.org/api/info?sitename=%s", sitename);
|
||||||
|
else
|
||||||
|
snprintf(url, sizeof(url), "https://neocities.org/api/info");
|
||||||
|
|
||||||
|
struct response resp;
|
||||||
|
int ret = perform_request(url, NULL, NULL, &resp, NULL);
|
||||||
|
if (ret) return ret;
|
||||||
|
|
||||||
|
json_error_t error;
|
||||||
|
json_t *root = json_loads(resp.data, 0, &error);
|
||||||
|
free(resp.data);
|
||||||
|
if (!root) return 3;
|
||||||
|
|
||||||
|
const char *result = json_string_value(json_object_get(root,"result"));
|
||||||
|
if (!result || strcmp(result,"success")!=0) { json_decref(root); return 4; }
|
||||||
|
|
||||||
|
json_t *info = json_object_get(root,"info");
|
||||||
|
if (!json_is_object(info)) {
|
||||||
|
json_decref(root);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t *j_sitename = json_object_get(info,"sitename");
|
||||||
|
json_t *j_created = json_object_get(info,"created_at");
|
||||||
|
json_t *j_updated = json_object_get(info,"last_updated");
|
||||||
|
json_t *j_domain = json_object_get(info,"domain");
|
||||||
|
json_t *j_hits = json_object_get(info,"hits");
|
||||||
|
|
||||||
|
out->sitename = json_is_string(j_sitename) ? strdup(json_string_value(j_sitename)) : NULL;
|
||||||
|
out->created_at = json_is_string(j_created) ? strdup(json_string_value(j_created)) : NULL;
|
||||||
|
out->last_updated = json_is_string(j_updated) ? strdup(json_string_value(j_updated)) : NULL;
|
||||||
|
out->domain = json_is_string(j_domain) ? strdup(json_string_value(j_domain)) : NULL;
|
||||||
|
out->hits = json_is_integer(j_hits) ? json_integer_value(j_hits) : 0;
|
||||||
|
|
||||||
|
json_t *tags = json_object_get(info,"tags");
|
||||||
|
if (!json_is_array(tags)) {
|
||||||
|
out->tags = NULL;
|
||||||
|
out->tag_count = 0;
|
||||||
|
} else {
|
||||||
|
size_t tc = json_array_size(tags);
|
||||||
|
out->tags = malloc(sizeof(char*) * tc);
|
||||||
|
out->tag_count = tc;
|
||||||
|
|
||||||
|
for (size_t i=0; i<tc; i++) {
|
||||||
|
json_t *tag = json_array_get(tags,i);
|
||||||
|
out->tags[i] = json_is_string(tag) ? strdup(json_string_value(tag)) : NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json_decref(root);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// list — GET /api/list
|
||||||
|
// -------------------------
|
||||||
|
int neocities_list(
|
||||||
|
const char *api_key,
|
||||||
|
const char *path, neocities_filelist_t *out
|
||||||
|
)
|
||||||
|
{
|
||||||
|
char url[512];
|
||||||
|
if (path) snprintf(url, sizeof(url), "https://neocities.org/api/list?path=%s", path);
|
||||||
|
else snprintf(url, sizeof(url), "https://neocities.org/api/list");
|
||||||
|
|
||||||
|
struct response resp;
|
||||||
|
int ret = perform_request(url, api_key, NULL, &resp, NULL);
|
||||||
|
if (ret) return ret;
|
||||||
|
|
||||||
|
json_error_t error;
|
||||||
|
json_t *root = json_loads(resp.data, 0, &error);
|
||||||
|
free(resp.data);
|
||||||
|
if (!root) return 3;
|
||||||
|
|
||||||
|
json_t *arr = json_object_get(root, "files");
|
||||||
|
if (!arr) { json_decref(root); return 4; }
|
||||||
|
|
||||||
|
size_t count = json_array_size(arr);
|
||||||
|
out->paths = malloc(sizeof(char*) * count);
|
||||||
|
out->count = count;
|
||||||
|
for (size_t i=0; i<count; i++) {
|
||||||
|
json_t *f = json_array_get(arr, i);
|
||||||
|
out->paths[i] = strdup(json_string_value(json_object_get(f,"path")));
|
||||||
|
}
|
||||||
|
json_decref(root);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// delete — POST /api/delete
|
||||||
|
// -------------------------
|
||||||
|
int neocities_delete(
|
||||||
|
const char *api_key,
|
||||||
|
const char **filenames,
|
||||||
|
size_t count,
|
||||||
|
char **response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
char body[1024] = "";
|
||||||
|
for (size_t i=0; i<count; i++) {
|
||||||
|
char tmp[256];
|
||||||
|
snprintf(tmp, sizeof(tmp), "filenames[]=%s&", filenames[i]);
|
||||||
|
strncat(body, tmp, sizeof(body)-strlen(body)-1);
|
||||||
|
}
|
||||||
|
struct response resp;
|
||||||
|
int ret = perform_request(
|
||||||
|
"https://neocities.org/api/delete",
|
||||||
|
api_key,
|
||||||
|
body,
|
||||||
|
&resp,
|
||||||
|
NULL
|
||||||
|
);
|
||||||
|
if (ret) return ret;
|
||||||
|
*response = resp.data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// upload — POST /api/upload
|
||||||
|
// (multipart/form-data)
|
||||||
|
// -------------------------
|
||||||
|
int neocities_upload(
|
||||||
|
const char *api_key,
|
||||||
|
const char **local_files,
|
||||||
|
const char **remote_names,
|
||||||
|
size_t count,
|
||||||
|
char **response
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!api_key || strlen(api_key) == 0)
|
||||||
|
return 3;
|
||||||
|
|
||||||
|
CURL *curl = curl_easy_init();
|
||||||
|
if (!curl) return 1;
|
||||||
|
|
||||||
|
struct response resp;
|
||||||
|
resp.data = malloc(1);
|
||||||
|
resp.len = 0;
|
||||||
|
resp.data[0] = '\0';
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, "https://neocities.org/api/upload");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 60L);
|
||||||
|
|
||||||
|
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERPWD, NULL);
|
||||||
|
|
||||||
|
struct curl_slist *headers = NULL;
|
||||||
|
char auth_header[512];
|
||||||
|
snprintf(auth_header, sizeof(auth_header),
|
||||||
|
"Authorization: Bearer %s", api_key);
|
||||||
|
|
||||||
|
headers = curl_slist_append(headers, auth_header);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
|
||||||
|
curl_mime *form = curl_mime_init(curl);
|
||||||
|
for (size_t i=0; i<count; i++) {
|
||||||
|
curl_mimepart *part = curl_mime_addpart(form);
|
||||||
|
curl_mime_name(part, remote_names[i]);
|
||||||
|
curl_mime_filedata(part, local_files[i]);
|
||||||
|
}
|
||||||
|
curl_easy_setopt(curl, CURLOPT_MIMEPOST, form);
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
curl_mime_free(form);
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
|
||||||
|
if (res != CURLE_OK) { free(resp.data); return 2; }
|
||||||
|
|
||||||
|
*response = resp.data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// get_api_key — GET /api/key
|
||||||
|
// -------------------------
|
||||||
|
// no. for security reasons of course.
|
||||||
38
src/neocities.h
Normal file
38
src/neocities.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef NEOCITIES_H
|
||||||
|
#define NEOCITIES_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Output structs
|
||||||
|
// -------------------------
|
||||||
|
typedef struct {
|
||||||
|
char *sitename;
|
||||||
|
int hits;
|
||||||
|
char *created_at;
|
||||||
|
char *last_updated;
|
||||||
|
char *domain;
|
||||||
|
char **tags;
|
||||||
|
size_t tag_count;
|
||||||
|
} neocities_info_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char **paths;
|
||||||
|
size_t count;
|
||||||
|
} neocities_filelist_t;
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// API functions
|
||||||
|
// -------------------------
|
||||||
|
int neocities_info(const char *sitename, neocities_info_t *out);
|
||||||
|
int neocities_list(const char *api_key, const char *path, neocities_filelist_t *out);
|
||||||
|
int neocities_delete(const char *api_key, const char **filenames, size_t count, char **response);
|
||||||
|
int neocities_upload(const char *api_key, const char **local_files, const char **remote_names, size_t count, char **response);
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// Free functions
|
||||||
|
// -------------------------
|
||||||
|
void neocities_free_info(neocities_info_t *info);
|
||||||
|
void neocities_free_filelist(neocities_filelist_t *list);
|
||||||
|
|
||||||
|
#endif // NEOCITIES_H
|
||||||
BIN
src/neocities.o
Normal file
BIN
src/neocities.o
Normal file
Binary file not shown.
Reference in New Issue
Block a user