├── 1. zites.md
├── 10. editing user data.md
├── 11. merger sites.md
├── 12. merged content.md
├── 13. ugly things.md
├── 2. security.md
├── 3. cloning zites.md
├── 4. creating static zite.md
├── 5. zeroframe.md
├── 6. databases.md
├── 7. changing tables.md
├── 8. dbschema.md
├── 9. user content.md
├── README.md
├── SUMMARY.md
├── downloads
├── ZeroAuth.js
├── ZeroFS.js
├── ZeroPage.js
├── blog.html
├── blog_library.zip
├── blog_modern.zip
├── blog_single.zip
├── notepad.html
├── notepad_single.zip
├── notepad_zerofs.zip
├── notepad_zeropage.zip
├── portfolio.zip
├── voting.html
├── voting_library.zip
├── voting_modern.zip
└── voting_single.zip
└── extra
├── 1. zeropage.md
└── 2. zeronet vs. others.md
/1. zites.md:
--------------------------------------------------------------------------------
1 | # ZeroNet sites
2 |
3 | ZeroNet sites, or *zites*, as we call them, are distributed across the network.
4 |
5 |
6 | ## Why ZeroNet is better than Internet
7 |
8 | ### Internet
9 |
10 | Here is the scheme of the Internet:
11 |
12 | ________ +--------+ | |
13 | | | | iPhone | | |
14 | | iPad | +--------+ | | ______________
15 | |________|-----------+ | | | |
16 | | | ________ | +--| Google |
17 | _________ | | | /..\ | | | |______________|
18 | | Windows |--------+-------|---| \__/ |---|--+ ______________
19 | |_________| | |___||___| | | | |
20 | |_|_|_|_|_| | | +--| Facebook |
21 | |_|_|_|_|_| | | |______________|
22 | | |
23 | *Devices* | *Provider* | *Sites*
24 |
25 | And that's what happens if your provider is down or the server you're trying to connect is down or blocked:
26 |
27 | ________ +--------+ | |
28 | | | | iPhone | | |
29 | | iPad | +--------+ | | ______________
30 | |________|-----------+ | | | |
31 | | | ________ | | Google |
32 | _________ | | |\/\/\/\/| | |______________|
33 | | Windows |--------+-------|---|/\ No /\| | ______________
34 | |_________| | |\/\/\/\/| | | |
35 | |_|_|_|_|_| | | | Facebook |
36 | |_|_|_|_|_| | | |______________|
37 | | |
38 | *Devices* | *Provider* | *Sites*
39 |
40 | You've got no access to the server.
41 |
42 |
43 | ### ZeroNet
44 |
45 | And this is ZeroNet:
46 |
47 | ________ +--------+
48 | | | | iPhone |
49 | | iPad | +--------+
50 | |________|-----------+
51 | | |
52 | __|______ |
53 | | Windows |--------+
54 | |_________|
55 | |_|_|_|_|_|
56 | |_|_|_|_|_|
57 |
58 | *Devices = zites*
59 |
60 | If *iPhone* is down or blocked:
61 |
62 | ________ +--------+
63 | | | |\/ No \/|
64 | | iPad | +--------+
65 | |________|
66 | |
67 | __|______
68 | | Windows |
69 | |_________|
70 | |_|_|_|_|_|
71 | |_|_|_|_|_|
72 |
73 | *Devices = zites*
74 |
75 | Other peers still have access to the zites.
76 |
77 |
78 | ## Installing ZeroNet
79 |
80 | To use ZeroNet on Windows, Mac or Linux machine, install it first. Visit `https://zeronet.io` and download ZeroNet there.
81 |
82 |
83 | ## Visiting zites
84 |
85 | ZeroNet installs a local server on your computer, on *http://127.0.0.1:43110* address. When you run ZeroNet, a browser window opens.
86 |
87 | On the left, you can see the list of zites hosted by your machine, and a few links to other helpful zites. Click zite to open (or download) it. Feel free to visit some of them now.
88 |
89 | I said *download* because that's how ZeroNet works. Let's discuss it later.
90 |
91 |
92 | ## Getting ZeroID
93 |
94 | A user can register on such *Internet* sites as Google, eBay, etc. On zites, registration is not technically possible, but don't worry. Instead, ZeroNet introduces *Certificate Authorities*. For example, [ZeroID](http://127.0.0.1:43110/1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz/). This is the most popular certificate provider, so let's register there first.
95 |
96 | Visit [ZeroID](http://127.0.0.1:43110/1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz/) and register with any nickname you like. (*Note: If you don't want to reveal your IP, set up ZeroNet to use TOR.*)
97 |
98 | You enterd nickname, but what about password? It was generated automatically. There is a `users.json` file in ZeroNet root directory. When you register on *ZeroID*, new entries appear in `users.json`: your public and private keys for *ZeroID*. You shouldn't share your private key - it's your password. And public key is your login - you share it with zites you post on. Your nickname is also stored there. If you lose `users.json` or manually change your public and private keys or nickname, you cannot access your account. It's a good idea to backup this file.
99 |
100 |
101 | ## Getting ZeroTalk account
102 |
103 | Now, let's check a zite which let you use your *ZeroID*. *ZeroTalk* is a perfect choice. It's a popular forum.
104 |
105 | Let's visit [ZeroTalk](http://127.0.0.1:43110/1TaLkFrMwvbNsooF4ioKAY9EuxTBTjipT/). Go to *Test messages* topic, press *Sign in as...* and choose *{yourusername}@zeroid.bit*. Enter some kind of `Hello, world!` and press *Submit comment*. Instead of `Hello, world!`, you can publish this tutorial. :)
106 |
107 | Notice `Forums: English · Dansk · Español · Français ...` on the top of the page and select your language. If you are from *China*, better go to the special [New GFW Talk](http://127.0.0.1:43110/19BPUZYAdCMxExKHoVSG3cG95wfUfFTEC9/) zite.
108 |
--------------------------------------------------------------------------------
/10. editing user data.md:
--------------------------------------------------------------------------------
1 | # Editing user data
2 |
3 | Now let's try to edit user content. Remember, we are going to write a voting system.
4 |
5 |
6 | ## `data.json` files
7 |
8 | Now let's write a function which will add a question.
9 |
10 |
11 | First of all, we will take our `js/files.js` program and include it to `index.html`:
12 |
13 | function readFile(file, callback) {
14 | zeroFrame.cmd("fileGet", [file, false], callback);
15 | }
16 |
17 | function writeFile(file, content, callback) {
18 | zeroFrame.cmd("fileWrite", [file, base64Encode(content)], callback);
19 | }
20 |
21 | function base64Encode(content) {
22 | content = encodeURIComponent(content); // Split to bytes in % notation
23 | content = unescape(content); // Join % notation as bytes (not as chars)
24 | return btoa(content);
25 | }
26 |
27 |
28 | Now, let's write `addQuestion()` function in `js/votes.js`:
29 |
30 | function addQuestion(question, answers, callback) {
31 | ...
32 | }
33 |
34 |
35 | If somebody wants to add a question, he has to be authorizated with ZeroID:
36 |
37 | function addQuestion(question, answers, callback) {
38 | authAsZeroID(function(user) {
39 | // User rejected to authorizate
40 | if(!user) {
41 | callback(false);
42 | return;
43 | }
44 |
45 | ...
46 | });
47 | }
48 |
49 |
50 | Let's open JSON file of user:
51 |
52 | function addQuestion(question, answers, callback) {
53 | authAsZeroID(function(user) {
54 | // User rejected to authorizate
55 | if(!user) {
56 | callback(false);
57 | return;
58 | }
59 |
60 | readFile("data/users/" + user.address + "/data.json", function(content) {
61 | content = content || "";
62 |
63 | // Parse JSON
64 | try {
65 | content = JSON.parse(content);
66 | } catch(e) {
67 | content = {
68 | questions: [],
69 | answers: {},
70 | next_question_id: 0
71 | };
72 | }
73 |
74 | ...
75 | });
76 | });
77 | }
78 |
79 | Notice that we try to open file with user address. User address is its public key for ZeroID. If user uses KaffieID, public key and username (`user` property) will be different.
80 |
81 |
82 | Now we add question, save JSON and publish user's `content.json`:
83 |
84 | function addQuestion(question, answers, callback) {
85 | authAsZeroID(function(user) {
86 | // User rejected to authorizate
87 | if(!user) {
88 | callback(false);
89 | return;
90 | }
91 |
92 | readFile("data/users/" + user.address + "/data.json", function(content) {
93 | content = content || "";
94 |
95 | // Parse JSON
96 | try {
97 | content = JSON.parse(content);
98 | } catch(e) {
99 | content = {
100 | questions: [],
101 | answers: {},
102 | next_question_id: 0
103 | };
104 | }
105 |
106 | var id = content.next_question_id;
107 | content.questions.push({
108 | id: id++,
109 | question: question,
110 | answers: answers.join("\n"),
111 | date_added: Math.floor(Date.now() / 1000)
112 | });
113 |
114 | content = JSON.stringify(content);
115 |
116 | writeFile("data/users/" + user.address + "/data.json", content, function() {
117 | zeroFrame.cmd("sitePublish", {
118 | inner_path: "data/users/" + user.address + "/content.json"
119 | }, function() {
120 | callback(id);
121 | });
122 | });
123 | });
124 | });
125 | }
126 |
127 | Notice that we don't pass `privatekey` to `sitePublish` command. That's because `privatekey: null` means "sign using user's private key".
128 |
129 | We also sign `data/users/{address}/content.json`, but it is possible that it does not exist (eg. user added question first time). Hopefully, ZeroNet creates a `content.json` for us if it does not exist.
130 |
131 |
132 | Let's check our code. Open DevTools, reload page and type the following in the console:
133 |
134 | addQuestion("What's nofish's name?", ["nofish", "Tomas", "Jack"], console.log.bind(console));
135 |
136 | Authorizate using ZeroID (if you are asked) and watch `data/votes.db`.
137 |
138 |
139 | ## Answers
140 |
141 | Try to write `addAnswer()` function for yourself first.
142 |
143 | Answer:
144 |
145 | function addQuestion(questionId, answerId, callback) {
146 | authAsZeroID(function(user) {
147 | // User rejected to authorizate
148 | if(!user) {
149 | callback(false);
150 | return;
151 | }
152 |
153 | readFile("data/users/" + user.address + "/data.json", function(content) {
154 | content = content || "";
155 |
156 | // Parse JSON
157 | try {
158 | content = JSON.parse(content);
159 | } catch(e) {
160 | content = {
161 | questions: [],
162 | answers: {},
163 | next_question_id: 0
164 | };
165 | }
166 |
167 | content.answers[questionId] = answerId;
168 |
169 | content = JSON.stringify(content);
170 |
171 | writeFile("data/users/" + user.address + "/data.json", content, function() {
172 | zeroFrame.cmd("sitePublish", {
173 | inner_path: "data/users/" + user.address + "/content.json"
174 | }, callback);
175 | });
176 | });
177 | });
178 | }
179 |
180 |
181 | Note that we have similar functions. Let's write a library function instead:
182 |
183 | function editUserData(handler, callback) {
184 | authAsZeroID(function(user) {
185 | // User rejected to authorizate
186 | if(!user) {
187 | callback(false);
188 | return;
189 | }
190 |
191 | readFile("data/users/" + user.address + "/data.json", function(content) {
192 | content = content || "";
193 |
194 | // Parse JSON
195 | try {
196 | content = JSON.parse(content);
197 | } catch(e) {
198 | content = {
199 | questions: [],
200 | answers: {},
201 | next_question_id: 0
202 | };
203 | }
204 |
205 | handler(content);
206 |
207 | content = JSON.stringify(content);
208 |
209 | writeFile("data/users/" + user.address + "/data.json", content, function() {
210 | zeroFrame.cmd("sitePublish", {
211 | inner_path: "data/users/" + user.address + "/content.json"
212 | }, callback);
213 | });
214 | });
215 | });
216 | }
217 |
218 | function addQuestion(question, answers, callback) {
219 | var id;
220 | editUserData(function(content) {
221 | id = content.next_question_id;
222 |
223 | content.questions.push({
224 | id: content.next_question_id++,
225 | question: question,
226 | answers: answers.join("\n"),
227 | date_added: Math.floor(Date.now() / 1000)
228 | });
229 | }, function() {
230 | callback(id);
231 | });
232 | }
233 |
234 |
235 | function addAnswer(questionId, answerId, callback) {
236 | editUserData(function(content) {
237 | content.answers[questionId] = answerId;
238 | }, callback);
239 | }
240 |
241 |
242 | ## Reading data
243 |
244 | Now we will write `getQuestionList()`, `getQuestion()` and `getAnswers()` functions:
245 |
246 | function getQuestionList(sort, callback) {
247 | if(sort == "popular") {
248 | zeroFrame.cmd("dbQuery", ["SELECT questions.*, CASE WHEN answers.answer_count IS NULL THEN 0 ELSE answers.answer_count END AS answer_count FROM questions LEFT JOIN (SELECT question_id, COUNT(*) as answer_count FROM answers GROUP BY question_id) AS answers ON (answers.question_id = questions.id) ORDER BY answers.answer_count DESC, questions.date_added DESC LIMIT 0, 10"], callback);
249 | } else if(sort == "latest") {
250 | zeroFrame.cmd("dbQuery", ["SELECT * FROM questions ORDER BY date_added DESC LIMIT 0, 10"], callback);
251 | }
252 | }
253 |
254 | function getQuestion(id, callback) {
255 | zeroFrame.cmd("dbQuery", ["SELECT * FROM questions WHERE id = " + id], function(questions) {
256 | zeroFrame.cmd("siteInfo", [], function(siteInfo) {
257 | if(siteInfo.cert_user_id) { // User logged in
258 | zeroFrame.cmd("dbQuery", ["SELECT answers.*, json.* FROM answers, json WHERE json.directory = \"users/" + siteInfo.auth_address + "\" AND answers.json_id = json.json_id AND answers.question_id = " + id], function(answer) {
259 | if(answer.length) {
260 | questions[0].answered = answer[0].answer_id;
261 |
262 | getAnswers(id, function(answers) {
263 | questions[0].answers = answers;
264 | callback(questions[0]);
265 | });
266 | } else {
267 | questions[0].answered = -1;
268 | callback(questions[0]);
269 | }
270 | });
271 | } else {
272 | questions[0].answered = -1;
273 | callback(questions[0]);
274 | }
275 | });
276 | });
277 | }
278 |
279 | function getAnswers(id, callback) {
280 | zeroFrame.cmd("dbQuery", ["SELECT answer_id, COUNT(*) as answer_count FROM answers WHERE question_id = " + id + " GROUP BY answer_id"], function(answers) {
281 | var result = {};
282 | for(var i = 0; i < answers.length; i++) {
283 | result[answers[i].answer_id] = answers[i].answer_count;
284 | }
285 | callback(result);
286 | });
287 | }
288 |
289 | Now, core is finished. Only layout is left. As usual, [here](downloads/voting.html) is an example.
290 |
--------------------------------------------------------------------------------
/11. merger sites.md:
--------------------------------------------------------------------------------
1 | # Merger sites
2 |
3 | Let's have a look at ZeroMe. It's a social network with about 10 000 users. And each user can write a lot of data. So, we would have to let ZeroMe use around 600 MB of storage. Really bad. Hopefully, ZeroNet introduces merger sites to solve this problem.
4 |
5 |
6 | ## Structure
7 |
8 | Imagine the following structure:
9 |
10 | ZeroMe = 500 MB
11 | |
12 | +-- data = 500 MB
13 | | |
14 | | +-- users = 500 MB
15 | | |
16 | | +-- user1 = 50 KB
17 | | +-- user2 = 50 KB
18 | | +-- user3 = 50 KB
19 | | +-- user4 = 50 KB
20 | | +-- user5 = 50 KB
21 | | +-- user...
22 | | +-- user10000 = 50 KB
23 | +-- index.js = 250 KB
24 | +-- index.css = 100 KB
25 | +-- index.html = 50 KB
26 |
27 | Here, users have to download all other users' data (`userN` directories).
28 |
29 |
30 | And the imagine this:
31 |
32 | ZeroMe = 400 KB
33 | |
34 | +-- index.js = 250 KB
35 | +-- index.css = 100 KB
36 | +-- index.html = 50 KB
37 |
38 | UserDB = 10 MB
39 | |
40 | +-- data = 10 MB
41 | | |
42 | | +-- userinfo = 10 MB
43 | | |
44 | | +-- user1 = 1 KB
45 | | +-- user2 = 1 KB
46 | | +-- user3 = 1 KB
47 | | +-- user4 = 1 KB
48 | | +-- user5 = 1 KB
49 | | +-- user...
50 | | +-- user10000 = 1 KB
51 | +-- index.html = 5 KB
52 |
53 | Hub1 = 167 MB
54 | |
55 | +-- data = 167 MB
56 | | |
57 | | +-- users = 167 MB
58 | | |
59 | | +-- user1 = 50KB
60 | | +-- user2 = 50KB
61 | | +-- user...
62 | | +-- user3333 = 50KB
63 | +-- index.html = 5 KB
64 |
65 | Hub2 = 167 MB
66 | |
67 | +-- data = 167 MB
68 | | |
69 | | +-- users = 167 MB
70 | | |
71 | | +-- user3334 = 50KB
72 | | +-- user3335 = 50KB
73 | | +-- user...
74 | | +-- user6666 = 50KB
75 | +-- index.html = 5 KB
76 |
77 | Hub3 = 167 MB
78 | |
79 | +-- data = 167 MB
80 | | |
81 | | +-- users = 167 MB
82 | | |
83 | | +-- user6667 = 50KB
84 | | +-- user6668 = 50KB
85 | | +-- user...
86 | | +-- user10000 = 50KB
87 | +-- index.html = 5 KB
88 |
89 | So, we have splitted data to different zites. That's how merger sites work. *ZeroMe* (`1MeFqFfFFGQfa1J3gJyYYUvb5Lksczq7nH` in reality) has only layout and code, *UserDB* (`1UDbADib99KE9d3qZ87NqJF2QLTHmMkoV`) user metadata, while other zites - *hubs* have other data. Of course, downloading these hubs is optional.
90 |
91 | For example, if you download `ZeroMe`, `UserDB` and `Hub1`, you can communicate with `user878` and `user678` and learn that there are also `user5412`, `user3453` and `user6789` and that they are assigned to `Hub2` and `Hub3` zites. If you want to say *Hello!* to `user6789` or read his posts, you have to download `Hub3`. But usually you want to read *something*, not *everything*, and you download only 1-2 hubs.
92 |
93 |
94 | ## Making merger site
95 |
96 | First of all, let's make a new, empty zite. Open sidebar and name it `PostHere`. Remove all content from `
` of `index.html`. We will have data in hubs, and code in main `PostHere` zite.
97 |
98 |
99 | ## Making hub
100 |
101 | Let's make a new zite for content. This site address is `1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ` for me.
102 |
103 | Change `index.html` to:
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
114 |
115 |
116 | Use PostHere to watch content of this site.
117 |
118 |
119 |
120 | Change zite title if you want to `Red Hub` and sign `content.json`.
121 |
122 |
123 | ## Configurating merger sites
124 |
125 | Merger site (`1CyNApZ4zp7k3SSXsrW54vEFMHHBpDy3nm`, in my case) and merged site (`1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ`) must agree to make connection.
126 |
127 |
128 | So, let's add the following to root `content.json` our merged site (`1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ`):
129 |
130 | "merged_type": "PostHere",
131 |
132 |
133 | Now create a directory `merged-PostHere` in the root of merger site. We want users to sign this folder, so add:
134 |
135 | "ignore": "merged-.*",
136 |
137 | ...somewhere to `content.json` of main site. To virtually fill it with merger site content, we request `Merger:PostHere` permission:
138 |
139 | +-------------------------------------------------------------------------+
140 | | wrapperPermissionAdd |
141 | +-------------------------------------------------------------------------+
142 | | Request new permission for site |
143 | +-------------------------+-----------------------------------------------+
144 | | Parameter | Description |
145 | +-------------------------+-----------------------------------------------+
146 | | permission | Name of permission |
147 | +-------------------------+-----------------------------------------------+
148 | | Return: "Granted" if allowed, not send when disallowed |
149 | +-------------------------------------------------------------------------+
150 |
151 |
152 | So, add the following to `js/index.js` file and add it to `index.html` of PostHere:
153 |
154 | window.zeroFrame = new ZeroFrame();
155 |
156 | function requestPermission(permission, callback) {
157 | zeroFrame.cmd("siteInfo", [], function(siteInfo) {
158 | // Already have permission
159 | if(siteInfo.settings.permissions.indexOf(permission) > -1) {
160 | callback();
161 | return;
162 | }
163 |
164 | zeroFrame.cmd("wrapperPermissionAdd", [permission], callback);
165 | });
166 | }
167 |
168 | requestPermission("Merger:PostHere", function() {
169 | // TODO
170 | });
171 |
172 |
173 | ## Accessing merged sites
174 |
175 | ZeroNet doesn't provide us a way to read and write other site's content, so MergerSite plugin makes all `merged-...` directories virtual. So to ZeroNet `merged-PostHere` structure is:
176 |
177 | merged-PostHere
178 | |
179 | +-- merger.db
180 | +-- 1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ
181 | |
182 | +-- js
183 | | |
184 | | +-- ZeroFrame.js
185 | +-- data
186 | | |
187 | | +-- users
188 | | |
189 | | +-- user1
190 | | | |
191 | | | +-- data.json
192 | | | +-- content.json
193 | | +-- content.json
194 | +-- index.html
195 |
196 | We have set `db_path` to `merged-PostHere/merger.db` because ZeroNet can gather data only from files which are in database directory, which is in this case `merged-PostHere`.
197 |
198 | So, if we want to access `data/users/{address}/data.json` of `1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ`, we have to access `merged-PostHere/1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ/data/users/{address}/data.json`.
199 |
200 | In the following part of the tutorial, we will fill our `PostHere` zite with user content.
201 |
--------------------------------------------------------------------------------
/12. merged content.md:
--------------------------------------------------------------------------------
1 | ## Editing merged sites
2 |
3 | In the previous part, we made a merger site and a merged site (hub) for our new zite, *PostHere*.
4 |
5 |
6 | ## Creating directory for user content
7 |
8 | First of all we have to create `data/users/content.json` in our hub. Nothing new here.
9 |
10 | {
11 | "ignore": ".*",
12 | "user_contents": {
13 | "cert_signers": {
14 | "zeroid.bit": ["1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz"]
15 | },
16 | "permission_rules": {
17 | ".*": {
18 | "files_allowed": "data.json",
19 | "max_size": 50000
20 | }
21 | },
22 | "permissions": {}
23 | }
24 | }
25 |
26 | - `ignore` - don't add users to `files` property.
27 | - `user_contents` - user-signed content.
28 | - `user_contents.cert_signers` - allowed *Certificate Authorities*.
29 | - `user_contents.permission_rules` - multiuser permissions.
30 | - `user_contents.permissions` - single user permissions.
31 |
32 | ...And include it to root `content.json`:
33 |
34 | ...
35 | "ignore": "data/users/.*",
36 | "includes": {
37 | "data/users/content.json": {
38 | "signers": [],
39 | "signers_required": 1
40 | }
41 | },
42 | ...
43 |
44 |
45 | Sign `content.json` and then `data/users/content.json`.
46 |
47 |
48 | ## Database schema
49 |
50 | As usual, we will use SQLite. Add this `dbschema.json` to main site:
51 |
52 | {
53 | "db_name": "merger",
54 | "db_file": "merged-PostHere/merger.db",
55 | "version": 3,
56 | "maps": {
57 | ".+/data/users/.+/content.json": {
58 | "to_json_table": ["cert_user_id"]
59 | },
60 | ".+/data/users/.+/data.json": {
61 | "to_table": [
62 | {
63 | "node": "posts",
64 | "table": "posts"
65 | }
66 | ]
67 | }
68 | },
69 | "tables": {
70 | "json": {
71 | "cols": [
72 | ["json_id", "INTEGER PRIMARY KEY AUTOINCREMENT"],
73 | ["site", "TEXT"],
74 | ["directory", "TEXT"],
75 | ["file_name", "TEXT"],
76 | ["cert_user_id", "TEXT"]
77 | ],
78 | "indexes": ["CREATE UNIQUE INDEX path ON json(directory, site, file_name)"],
79 | "schema_changed": 2
80 | },
81 | "posts": {
82 | "cols": [
83 | ["id", "integer"],
84 | ["title", "text"],
85 | ["content", "text"],
86 | ["date_added", "integer"],
87 | ["json_id", "integer references json(json_id)"]
88 | ],
89 | "indexes": [
90 | "CREATE UNIQUE INDEX post_id ON posts(id)"
91 | ],
92 | "schema_changed": 2
93 | }
94 | }
95 | }
96 |
97 | - Have a look at `version: 3`. This means: `json` table will now have `site` column, too. For merger sites, `site` is hub address. We also use `merged-PostHere` directory for database. This will be described later.
98 | - Look at `.+/data/users/.+/content.json` object. It has `to_json_table` array with `["cert_user_id"]`. `json` table also has `cert_user_id` column. `to_json_table` means: "take `cert_user_id` property from file and use it for `cert_user_id` column of `json` table for this json". `cert_user_id` is username.
99 |
100 |
101 | ### Getting username
102 |
103 | So, sample content is:
104 |
105 | +-------------------------------------------------------------------------+
106 | | json |
107 | +---------+---------+-------------------+--------------+------------------+
108 | | json_id | site | directory | file_name | cert_user_id |
109 | +---------+---------+-------------------+--------------+------------------+
110 | | 1 | 1Red... | data/users/1Cv... | data.json | NULL |
111 | +---------+---------+-------------------+--------------+------------------+
112 | | 2 | 1Red... | data/users/1CV... | content.json | ivanq@zeroid.bit |
113 | +---------+---------+-------------------+--------------+------------------+
114 | +-------------------------------------------------------------------------+
115 | | posts |
116 | +---------+-------------+--------------------------+------------+---------+
117 | | id | title | content | date_added | json_id |
118 | +---------+-------------+--------------------------+------------+---------+
119 | | 1 | Hello world | My first post in this... | 1234567890 | 1 |
120 | +---------+-------------+--------------------------+------------+---------+
121 |
122 | Everything is OK until you want to get post author. For each post, you have to take `json_id`, then get `directory` column of this `json_id` in `json` table, then find a row in `json` table which has `directory` = `directory` of old `json_id` and `file_name` = `content.json` and then get `cert_user_id`. So, simplest DB query is:
123 |
124 | SELECT posts.*, json2.cert_user_id as username FROM posts, json, json AS json2 WHERE json.directory = json2.directory AND json2.file_name = "content.json" AND posts.json_id = json.json_id
125 |
126 | Hopefully, ZeroNet has another option for this.
127 |
128 | ...
129 | ".+/data/users/.+/content.json": {
130 | "to_json_table": ["cert_user_id"],
131 | "file_name": "data.json"
132 | },
133 | ...
134 |
135 | `file_name` works like `JOIN`. For each user's `content.json`, ZeroNet find a row with same `site` and `directory` properties and with `file_name` = `data.json` and adds `cert_user_id` to this row. So, now structure is:
136 |
137 | +-------------------------------------------------------------------------+
138 | | json |
139 | +---------+---------+-------------------+--------------+------------------+
140 | | json_id | site | directory | file_name | cert_user_id |
141 | +---------+---------+-------------------+--------------+------------------+
142 | | 1 | 1Red... | data/users/1Cv... | data.json | ivanq@zeroid.bit |
143 | +---------+---------+-------------------+--------------+------------------+
144 | +-------------------------------------------------------------------------+
145 | | posts |
146 | +---------+-------------+--------------------------+------------+---------+
147 | | id | title | content | date_added | json_id |
148 | +---------+-------------+--------------------------+------------+---------+
149 | | 1 | Hello world | My first post in this... | 1234567890 | 1 |
150 | +---------+-------------+--------------------------+------------+---------+
151 |
152 | ...and we can simplify our query:
153 |
154 | SELECT posts.*, json.cert_user_id AS username FROM posts, json WHERE posts.json_id = json.json_id
155 |
156 |
157 | So, now you can use your memory or previous sections to write the rest of the code. As you remember, ZeroNet uses virtual directories, for merger sites, so there is nothing new here expect file we change: `merged-PostHere/{hub}/data/users/{address}/data.json`.
158 |
159 |
160 | ## Adding and removing hubs
161 |
162 | We can show users a list of hubs they doesn't have with `mergerSiteList` command.
163 |
164 | +-------------------------------------------------------------------------+
165 | | mergerSiteList |
166 | +-------------------------------------------------------------------------+
167 | | Return merged sites. |
168 | +-------------------------+-----------------------------------------------+
169 | | Parameter | Description |
170 | +-------------------------+-----------------------------------------------+
171 | | query_site_info | If True, then gives back detailed site info |
172 | | | for merged sites |
173 | +-------------------------+-----------------------------------------------+
174 | | Return: List of merger sites as object |
175 | +-------------------------------------------------------------------------+
176 |
177 | Try to execute `zeroFrame.cmd("mergerSiteList", [false], console.log.bind(console));` You'll see `Object { 1RedXn7jxM23y4WsR7ByWzhjFaCcBJwVQ: "PostHere" }` or something like that in the console.
178 |
179 | These commands are also often used by merger sites:
180 |
181 | +-------------------------------------------------------------------------+
182 | | mergerSiteAdd |
183 | +-------------------------------------------------------------------------+
184 | | Start downloading new merger site (requires confirmation if called |
185 | | twice in 10 seconds) |
186 | +-------------------------+-----------------------------------------------+
187 | | Parameter | Description |
188 | +-------------------------+-----------------------------------------------+
189 | | addresses | Site address or list of site addresses |
190 | +-------------------------+-----------------------------------------------+
191 | | Return: Always "ok" (even before site is added) |
192 | +-------------------------------------------------------------------------+
193 |
194 | +-------------------------------------------------------------------------+
195 | | mergerSiteDelete |
196 | +-------------------------------------------------------------------------+
197 | | Stop seeding and delete a merged site |
198 | +-------------------------+-----------------------------------------------+
199 | | Parameter | Description |
200 | +-------------------------+-----------------------------------------------+
201 | | address | Site address |
202 | +-------------------------+-----------------------------------------------+
203 | | Return: "ok" or object with "error" property |
204 | +-------------------------------------------------------------------------+
205 |
206 |
207 | ## Example
208 |
209 | As usually, you can watch finished site [here](downloads/posthere.html) and hub [here](downloads/postherehub.html).
210 |
--------------------------------------------------------------------------------
/13. ugly things.md:
--------------------------------------------------------------------------------
1 | # Ugly things
2 |
3 | This part is for developers who use ZeroNet and ZeroFrame API often. Here I'll talk about some strange things in ZeroFrame API which I know only because I read source code.
4 |
5 |
6 | ## `dbQuery`
7 |
8 | ### `?` placeholder
9 |
10 | From ZeroMe source code:
11 |
12 | return Page.cmd("dbQuery", [
13 | "SELECT * FROM json WHERE ?", {
14 | directory: directory
15 | }
16 | ], function(subject_rows) {
17 |
18 | That is equal to (See note 1):
19 |
20 | SELECT * FROM json WHERE
21 | directory = ${directory}
22 |
23 | You can also use multiple key/value pairs, that means:
24 |
25 | SELECT * FROM json WHERE
26 | key1 = ${value1} AND
27 | key2 = ${value2} AND
28 | key3 = ${value3}
29 |
30 |
31 | If you want to make some condition negative, prepend key with `not__`:
32 |
33 | Page.cmd("dbQuery", [
34 | "SELECT * FROM json WHERE ?", {
35 | not__json_id: 2,
36 | file_name: "data.json"
37 | }
38 | ], handler);
39 |
40 | SELECT * FROM json WHERE
41 | json_id != 2 AND
42 | file_name = "data.json"
43 |
44 |
45 | You can also use array as value: (See note 2)
46 |
47 | Page.cmd("dbQuery", [
48 | "SELECT * FROM json WHERE ?", {
49 | not__json_id: [2, 3],
50 | file_name: ["content.json", "data.json"]
51 | }
52 | ], handler);
53 |
54 | SELECT * FROM json WHERE
55 | json_id NOT IN (2, 3) AND
56 | file_name IN ("content.json", "data.json")
57 |
58 |
59 | ### `:` placeholder
60 |
61 | But what if you want to use some special rules (like `A=a and (B=b or C=c)`) but still want to use placeholders? Use `:`!
62 |
63 | From ZeroMe source code:
64 |
65 | return Page.cmd("dbQuery", ["SELECT * FROM json WHERE site = :site AND directory = :directory LIMIT 1", params], (function(_this) {
66 |
67 | Pass parameters as usual (object as second argument) and then use `:{key}` placeholder to get that value!
68 |
69 |
70 | ## User content
71 |
72 | Remember this `data/users/content.json` we often use?
73 |
74 | {
75 | "files": {},
76 | "ignore": ".*",
77 | "user_contents": {
78 | "cert_signers": {
79 | "zeroid.bit": ["1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz"]
80 | },
81 | "permission_rules": {
82 | ".*": {
83 | "files_allowed": "data.json",
84 | "max_size": 50000
85 | }
86 | },
87 | "permissions": {}
88 | }
89 | }
90 |
91 | Currently only site admin can sign user content (eg. delete posts). And what if we want to add moderators?
92 |
93 | ...
94 | "permission_rules": {
95 | ".*": {
96 | "files_allowed": "data.json",
97 | "max_size": 50000,
98 | "signers": ["1Abc"]
99 | }
100 | },
101 | ...
102 |
103 | We add `signers` to `permission_rules` items! Now, `1Abc` can sign all users' content. And if you move `signers` to a new item, `.*bot@.*`, `1Abc` will be able to sign only `...bot` user content.
104 |
105 |
106 | ## Notes
107 |
108 | ### Note 1
109 |
110 | From `Db/DbCursor.py` of ZeroNet source code:
111 |
112 | def execute(self, query, params=None):
113 | if isinstance(params, dict) and "?" in query: # Make easier select and insert by allowing dict params
114 | if query.startswith("SELECT") or query.startswith("DELETE") or query.startswith("UPDATE"):
115 | # Convert param dict to SELECT * FROM table WHERE key = ? AND key2 = ? format
116 | query_wheres = []
117 |
118 | ### Note 2
119 |
120 | From `Db/DbCursor.py` of ZeroNet source code:
121 |
122 | if type(value) is list:
123 | if key.startswith("not__"):
124 | query_wheres.append(key.replace("not__", "") + " NOT IN (" + ",".join(["?"] * len(value)) + ")")
125 | else:
126 | query_wheres.append(key + " IN (" + ",".join(["?"] * len(value)) + ")")
127 |
--------------------------------------------------------------------------------
/2. security.md:
--------------------------------------------------------------------------------
1 | # Security
2 |
3 | This section is about security in ZeroNet. If you are not interested in this or don't understand much, just remember a few things and skip this part.
4 |
5 | - Each zite has a public key and a private key. You access *ZeroHello* by visiting `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D/` - here, `1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D` is a public key. Only site owner has private key, and he publishes changes using it.
6 | - When you want to download a zite, you connect to other peers you know and download zite content from them.
7 | - No one can change any data on zite without a private key for that zite.
8 |
9 |
10 | ## Wrong SHA512
11 |
12 | In ZeroNet, everybody hosts zites, but not all - only the zites they want. For example, if `Windows` hosts `ZeroMe`, and `iPad` wants to download it, `iPad` connects to `Windows` *directly* and says:
13 |
14 | - **iPad**: Hey, *Windows*, give me `content.json` of `ZeroMe`.
15 | - **Windows**: Here it is: ...
16 | - **iPad**: Thanks! And I also need `index.html`, `js/all.js` and `css/all.css` of `ZeroMe`.
17 | - **Windows**: Here is `index.html`: ... But I have no `js/all.js` and `css/all.css`, sorry.
18 |
19 | *Windows* gave *iPad* `index.html`, but where can *iPad* find `js/all.js` and `css/all.css`? From other peers *Windows* knows. But the problem is, if *Windows* is malicious, he can only recommend peers that also didn't have those files.
20 |
21 | - **iPad**: Good bye, *Windows*.
22 | - **iPad**: ...Searching peers...
23 | - **iPad**: Hey, *MacOS*, you have `js/all.js` and `css/all.css` of `ZeroMe`?
24 | - **MacOS**: Of course I have! Here they are: ...
25 |
26 | But *MacOS* is a hacker! It gives wrong `js/all.js`, with an exploit. Boom! `Windows` has an infected `js/all.js`!
27 |
28 | ...In fact, no. Each zite has its public key and private key. Public key example is: `1BewKAyyiMZHY3AjQn65J6f6Rcb9p1h64K`. Public key is zite address.
29 |
30 | In `content.json` of any zite, there is an SHA512 of each files and size of each file. **MacOS** gave **iPad** a file with other SHA512, so **iPad** says:
31 |
32 | - **iPad**: `css/all.css` is OK. `js/all.js`... isn't.
33 | - **iPad**: ...Adds *MacOS* to black list...
34 | - **iPad**: Good bye, *MacOS*.
35 | - **iPad**: ...Searching peers...
36 | - **iPad**: Hey, *Linux*, you have `js/all.js` of `ZeroMe`?
37 | - **Linux**: Yes. Here it is: ...
38 | - **iPad**: Thanks. Good bye, *Linux*.
39 |
40 | Now *iPad* has full *ZeroMe* and can show it to you.
41 |
42 | That's how ZeroNet makes data secure with SHA512.
43 |
44 |
45 | ## Wrong `content.json`
46 |
47 | Now *Linux* needs *ZeroTalk* (its public key is `1TaLkFrMwvbNsooF4ioKAY9EuxTBTjipT`):
48 |
49 | - **Linux**: You have `content.json` of `1TaLkFrMwvbNsooF4ioKAY9EuxTBTjipT`?
50 | - **MacOS**: Yes. Take it, please: ...
51 |
52 | As we know, *MacOS* is a hacker. It gives wrong `content.json`, with wrong SHA512. *Linux* would ask *MacOS* for `js/all.js`, and *MacOS* would give *Linux* file with exploit.
53 |
54 | - **Linux**: Thanks! ...Wait a minute. `content.json` is signed with wrong key.
55 | - **Linux**: ...Adds *MacOS* to black list...
56 | - **Linux**: Good bye, *MacOS*.
57 |
58 | *Linux* is clever. `content.json` is signed with private key. *MacOS* would have to steal private key or bruteforce it. But the latter is practically impossible. And you shouldn't let anybody steal zite's private key.
59 |
60 |
61 | ## Signing
62 |
63 | Now, we know how data is signed. Only `content.json` is signed, and all other files are signed using `content.json` and its SHA512. It means that nothing can be changed without private key, and usual rainbow tables are useless.
64 |
65 |
66 | ## Restricted policy
67 |
68 | As ZeroNet cannot create a new server for every zite you visit, it creates a single server at `http://127.0.0.1:43110/`. So, some more security is needed. ZeroNet opens every zite in a restricted `