Commit ba829193 authored by Evgeniy Zdravov's avatar Evgeniy Zdravov
Browse files

initial commit

parents
/tinysite.mapdb
/tinysite.mapdb.*
/autogenerated_admin_password.txt
/nb-configuration.xml
/nbactions.xml
# Авторизация
POST /api/auth
## Запрос
* username
* password
пример:
username=vasya&password=123yadebil
## Ответ
{ "success": true, "token": "aiLu9mae7er5eexei4quehaevieYiduquae2fahGh5onaighiep3waiGuNeiThuo" }
или
{ "success": false, "error": "пнх" }
# Получить список постов
GET /api/feed
или
GET /api/feed?category=новости
* category - категория
* after - мин. дата
* before - макс. дата
* limit - макс. кол-во
пагинация тут наверное будет не к месту
## Ответ
[
{
"author": "vasya",
"title": "Вася устал",
"type": "text/plain",
"body": "Вася устал и пошел спать",
"date": 1537052572300,
"categories": ["новости"]
},
{
"author": "vasya",
"title": "Вася поспал",
"type": "text/plain",
"body": "Вася отдавил слепого",
"date": 1538052572300,
"categories": ["новости"]
}
]
type - "text/plain" пока, в перспективе "text/markdown" или "text/html"
title - опциональное поле (пока не решил, нужно ли оно)
date - миллисекунды от начала эпохи
# Запостить
POST /api/post
## Поля запроса
* token - токен из авторизации
* title - заголовок
* type - тип контента
* body - контент
* categories - категории, разделённые \n (из js как-то так: ). для каждого поста должна быть указана как минимум 1 категория
## Ответ
{ "success": true, "date": 1537052572300, "slug": "vasya-ustal" }
или
{ "success": false, "error": "пнх" }
MIT License
Copyright (c) 2018 Evgeniy Zdravov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Tinysite Blog Engine
Tinysite is an attempt to create minimalistic secure blog engine for running a darknet blog site. It lacks many features, but allows new message posting, using categories.
## Quick Build
mvn compile package assembly:single
## Quick Start
java -jar target/tinysite-1.0-SNAPSHOT-jar-with-dependencies.jar
## Notes
### Categories
To specify root category in post editor, that is displayed on index page add "/" to categories list. Tinysite looks for optional `tinysite.properies` file for configuration options.
### Password
Password for user `admin` is generated automatically on first launch and stored in plaintext to `autogenerated_admin_password.txt`.
# Блоговый движок Tinysite
Этот софт делался на скорую руку, но оказался не нужным. Выбрасывать жалко, потому решил выложить на случай, если кому пригодится. Много чего не было доделано, но постить сообщения можно, добавлять разделы - тоже. Задача стояла такая: надо сделать максимально быстро софт для использования в даркнетах. То есть безопасный и без привязки к разным сторонним ресурсам, вроде e-mail провайдеров. Безопасность отчасти обеспечить было решено через минимализм. Хоть никакого аудита и не было, но думаю вышло неплохо. Сейчас там есть толко возможность постить соощения и отображать их по категориям. Пользователей добавлять можно через линк `/magic`, там пользователь `admin` может творить разную чёрную магию через запуск JavaScript с доступом к данным движка. Для сборки используем maven: `mvn compile package assembly:single`, для запуска команду вроде `java -jar tinysite-1.0-SNAPSHOT-jar-with-dependencies.jar`.
## Примечания
Для того, чтобы постить на гравную страницу надо указать категорию `"/"`, это алиас для "главной" категории. При запуске Tinysite ищет файл tinysite.properties для того, чтобы прочитать настройки оттуда.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitlab</groupId>
<artifactId>tinysite</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>10</maven.compiler.source>
<maven.compiler.target>10</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mapdb</groupId>
<artifactId>mapdb</artifactId>
<version>3.0.7</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>com.github.slugify</groupId>
<artifactId>slugify</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-template-velocity</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>twitter-text</artifactId>
<version>1.14.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.gitlab.tinysite.app.WebApp</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
/*
* Copyright 2018 Evgeniy Zdravov <vkbaakre@vfemail.net>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
*/
package com.gitlab.tinysite;
import com.twitter.Autolink;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONAware;
import org.json.simple.JSONValue;
import com.gitlab.util.Base58;
import com.gitlab.util.ExternalizeUtil;
/**
*
* @author Evgeniy Zdravov <vkbaakre@vfemail.net>
*/
public class Article implements Externalizable, JSONAware {
private String authorId;
private String title;
private String type;
private String body;
private String[] categories = EMPTY_STRING_ARRAY;
private long timestampMillis = 0;
private static String[] EMPTY_STRING_ARRAY = new String[0];
@Override
public void writeExternal(ObjectOutput out) throws IOException {
LinkedHashMap m = new LinkedHashMap();
m.put("authorId", authorId);
m.put("title", title);
m.put("type", type);
m.put("body", body);
m.put("categories", Arrays.asList(categories));
m.put("timestamp", timestampMillis);
ExternalizeUtil.writeAsJSON(out, m);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
Map<String, Object> m = ExternalizeUtil.readInJSON(in);
authorId = (String) m.get("authorId");
title = (String) m.get("title");
type = (String) m.get("type");
body = (String) m.get("body");
categories = ((List<String>) m.get("categories")).toArray(new String[0]);
timestampMillis = ((Number)m.get("timestamp")).longValue();
}
public String getAuthorId() {
return authorId;
}
public void setAuthorId(String authorId) {
this.authorId = authorId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String[] getCategories() {
return categories;
}
public void setCategories(String[] categories) {
this.categories = categories;
}
public long getTimestampMillis() {
return timestampMillis;
}
public void setTimestampMillis(long timestamp) {
this.timestampMillis = timestamp;
}
public Date getTimestamp() {
return timestampMillis == 0 ? null : new Date(timestampMillis);
}
public String calculateHash() {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
throw new Error(ex);
}
if (title != null) {
md.update(title.getBytes(StandardCharsets.UTF_8));
}
return Base58.encode(md.digest(body.getBytes(StandardCharsets.UTF_8)));
}
public String calculateId() {
if (timestampMillis == 0) {
throw new IllegalStateException();
}
return calculateHash() + "-" + timestampMillis;
}
private String cachedHtml = null;
public String getHtml() {
switch (type) {
case "text/html":
return body;
default:
if (cachedHtml != null)
return cachedHtml;
String s = body;
s = body
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\n", "</p><p>")
.replace("<p></p>", "");
s = new Autolink().autoLinkURLs(s);
return cachedHtml = "<p>"
+ s
+ "</p>";
}
}
@Override
public String toJSONString() {
Map m = new LinkedHashMap();
m.put("author", authorId);
if (title != null && !title.isEmpty()) {
m.put("title", title);
}
m.put("type", type);
m.put("body", body);
m.put("date", timestampMillis);
m.put("categories", Arrays.asList(categories));
return JSONValue.toJSONString(m);
}
}
/*
* Copyright 2018 Evgeniy Zdravov <vkbaakre@vfemail.net>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
*/
package com.gitlab.tinysite;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import com.gitlab.util.ExternalizeUtil;
import com.gitlab.util.Slugifier;
/**
*
* @author Evgeniy Zdravov <vkbaakre@vfemail.net>
*/
public class Category implements Externalizable {
public static final String MAIN_CATEGORY_SLUG = "";
public static final String MAIN_CATEGORY_TITLE = "";
public static final String MAIN_CATEGORY_ALIAS = "/";
private String title;
private String description;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
LinkedHashMap m = new LinkedHashMap();
m.put("title", title);
m.put("description", description);
ExternalizeUtil.writeAsJSON(out, m);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
Map<String, String> m = ExternalizeUtil.readInJSON(in);
title = m.get("title");
description = m.get("description");
}
@Override
public int hashCode() {
int hash = 7;
hash = 17 * hash + Objects.hashCode(this.title);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Category other = (Category) obj;
if (!Objects.equals(this.title, other.title)) {
return false;
}
return true;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
if (MAIN_CATEGORY_ALIAS.equals(title.trim().toLowerCase())) {
this.title = MAIN_CATEGORY_TITLE;
}
else {
this.title = title;
}
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public static String getSlug(String title) {
if (MAIN_CATEGORY_TITLE.equals(title)) {
return MAIN_CATEGORY_SLUG;
}
if (title.matches("^\\d+$")) {
return "_" + Slugifier.slugify(title);
}
return Slugifier.slugify(title);
}
public String getSlug() {
return getSlug(title);
}
public boolean isMain() {
return MAIN_CATEGORY_TITLE.equals(title);
}
}
/*
* Copyright 2018 Evgeniy Zdravov <vkbaakre@vfemail.net>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
*/
package com.gitlab.tinysite;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.gitlab.util.Slugifier;
/**
*
* @author Evgeniy Zdravov <vkbaakre@vfemail.net>
*/
public class DbArticles {
private StorageEngine storageEngine;
private DbUsers users;
public Map<String, Category> slugCategories;
public Map<String, Article> articles;
public Map<String, List<String>> categoryArticles = new ConcurrentHashMap<>();
public DbArticles(DbUsers users, StorageEngine storageEngine) {
this.storageEngine = storageEngine;
this.users = users;
slugCategories = storageEngine.map("categories");
articles = storageEngine.map("articles");
}
public Category createCategoryFromTitle(String title) {
return slugCategories.computeIfAbsent(Slugifier.slugify(title), (title_) -> {
Category category = new Category();
category.setTitle(title);
return category;
});
}