-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevref.html
More file actions
407 lines (368 loc) · 25.3 KB
/
devref.html
File metadata and controls
407 lines (368 loc) · 25.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<html lang="en">
<head>
<meta name="theme-color" content="#ffffff">
<meta name=viewport content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="shortcut icon" href="/favicon.ico" type="type=image/x-icon">
<link rel="stylesheet" href="/style/bootstrap.min.css">
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/style/main.css">
<title>
Soft:3proxy:Developer Reference </title>
<meta name="Description" content="3proxy freeware proxy server for Windows and Unix. HTTP, SOCKS, FTP, POP3">
<meta name="keywords" content="3proxy, proxy, proxy server, free, freeware, socks, socks v5">
<meta NAME="Author" content="3APA3A">
</head>
<body>
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark">
<a class="navbar-brand" href="/"><span class="nav-logotext"><span class="nav-logotext1">3</span> PROXY</span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Downloads
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="/download/">Downloads</a>
<a class="dropdown-item" href="/download/stable/">Stable version</a>
<a class="dropdown-item" href="/download/devel/">Development version</a>
<a class="dropdown-item" href="https://github.com/z3APA3A/3proxy">GIT</a>
<a class="dropdown-item" href="https://hub.docker.com/repository/docker/3proxy/3proxy">Docker</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Documentation
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="/documents/">Documentation</a>
<a class="dropdown-item" href="/doc/">Manual</a>
<a class="dropdown-item" href="/howtoe.html">HowTo</a>
<a class="dropdown-item" href="/plugins/">Plugins</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
About
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="/">About</a>
<a class="dropdown-item" href="https://github.com/z3APA3A/3proxy/issues">Questions and issues</a>
<a class="dropdown-item" href="/donations/">Help the project</a>
</div>
</li>
</ul>
</div>
<div class="lang-toogle nav navbar navbar-right navbar-dark bg-dark">
<a href="" lang="ru" id="lang-switch">RU</a><span>|</span><span lang="en">EN</span>
</div>
<script>document.getElementById('lang-switch').href='https://3proxy.ru'+location.pathname;</script>
</nav>
<div class="container container-content">
<div class="info-block">
<div class="alert alert-warning">
<a href="#" class="close close-info-block" data-bs-dismiss="alert" aria-label="close">×</a>
<strong>
Few antiviral products inadequately detect 3proxy as Trojan.Daemonize,
Backdoor.Daemonize, etc and many detect 3proxy as a PUA (potentially unwanted program).
It may cause browser warning on download page.
3proxy is not trojan or backdoor and contains
no functionality except described in documentation. Clear explanation of this
fact is given, for example, in
<A class="tiny" HREF="https://www.microsoft.com/en-us/wdsi/threats/malware-encyclopedia-description?Name=Program:Win32/TinyProxy" target="_blank">Microsoft</A>'s
article.
</strong>
</div>
</div>
<script type="text/javascript">
$(document).ready(function() {
// info-block
if (localStorage.getItem('show-info-block') == null) {
$('.info-block').show('slow');
} else {
$('.info-block').hide('slow');
}
$('.close-info-block').on('click', function(){
localStorage.setItem('show-info-block', '0');
})
// code highlighting
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
});
</script><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Style-Type" content="text/css">
<title>3proxy developer reference</title>
<style type="text/css">
span.s1 {text-decoration: underline}
ol {list-style-type: none}
ol.level1 {counter-reset: h1}
ol.level1 > li::before {content: counter(h1) ". "; counter-increment: h1; font-weight: bold}
ol.level2 {counter-reset: h2}
ol.level2 > li::before {content: counter(h1) "." counter(h2) " "; counter-increment: h2}
ol.level3 {counter-reset: h3}
ol.level3 > li::before {content: counter(h1) "." counter(h2) "." counter(h3) " "; counter-increment: h3}
pre {margin: 0.5em 0; padding: 0.5em; background: #f5f5f5; border: 1px solid #ddd}
code {font-family: monospace}
</style>
</head>
<body>
<h2>3proxy developer reference</h2>
<ol class="level1">
<li><b>Understanding Internal 3proxy structure</b>
<p>3proxy is implemented as multithread application. Server model is implemented as "one connection – one thread". It means, for every client connection new thread is created. This model is effective enough under Windows, because it allows it avoid thread creation on asynchronous operations, yet under most POSIX systems this model can not be considered as most efficient. It's planned for (very far in future) release to implement more efficient model, where single thread can serve few clients.</p>
<ol class="level2">
<li>main thread
<p>3proxy begins with main thread. This thread parses configuration file and starts main loop. During configuration file parsing <code>struct extaparam conf;</code> structure is filled and service threads are started.</p>
<p>Main loop cycle takes approximately 1 second and does these tasks:</p>
<ul>
<li>re-reads configuration file, if necessary</li>
<li>performs scheduled tasks</li>
<li>monitors files ('monitor' command), approx. once in a minute</li>
<li>rotates main logfile</li>
<li>dumps counters to file, approx. once in a minute</li>
<li>performs termination, if required</li>
</ul>
<p>It's guaranteed every configuration and schedule command is executed from the same thread.</p>
<p>Main thread is implemented in <b>3proxy.c</b></p>
</li>
<li>service thread
<p>Service threads are started immediately, than service command (e.g. 'proxy' or 'socks') are found during configuration file parsing. Each command creates new thread. Thread does these tasks:</p>
<ul>
<li>parses service command arguments and fills <code>struct srvparam srv</code> structure with service configuration and <code>struct clientparam defparam</code> structure with default client configuration</li>
<li>initializes filters (filter_open)</li>
<li>creates and initializes listening service socket</li>
<li>enters into service loop</li>
<li>terminates filters (filter_close)</li>
</ul>
<p>service loop:</p>
<ul>
<li>checks for configuration reload (approximately every second), thread exits if configuration reloaded or 3proxy is in terminating state.</li>
<li>accepts client connection and creates <code>struct clientparam newparam</code> structure with client configuration</li>
<li>creates/checks client filters (filter_client)</li>
<li>creates client thread with newly created <code>struct clientparam newparam</code></li>
</ul>
<p>service threads are implemented in <b>proxymain.c</b></p>
<p>Please note: struct clientparam is freed and filter_clear is executed from different (client) thread.</p>
</li>
<li>client thread
<p>Client threads are started from service thread. Client thread:</p>
<ul>
<li>reads client request (except portmappers) with authentication information and request headers (if any).</li>
<li>filters request (if any) with filter_request</li>
<li>filters headers (if any) with filter_header_cli</li>
<li>performs authentication and authorization</li>
<li>established connection with server</li>
<li>sends request to server</li>
<li>filters server headers (if any)</li>
<li>maps client end server sockets to transmit data between client and server</li>
<li>logs request. Global counters are also updated on this operation</li>
<li>clears client filters (filter_clear)</li>
<li>frees <code>struct clientparam</code> data</li>
</ul>
<p>in some point client thread may loop to process few client requests from the same connection (e.g. HTTP 'established' connection in 'proxy').</p>
<p>Socket mapping does:</p>
<ul>
<li>caches data in internal client and server buffers</li>
<li>delays data transmit to limit bandwidth</li>
<li>performs data filtering (filter_data_cli / filter_data_srv)</li>
</ul>
<p>client threads are implemented in <b>proxy.c</b>, <b>socks.c</b>, <b>pop3p.c</b> etc.</p>
</li>
</ol>
</li>
<li><b>Hacking into 3proxy code with plugins</b>
<ol class="level2">
<li>What is 3proxy plugin
<p>3proxy plugin is any dynamic/shared library. There is no specific requirement for plugin, actually you can load any dynamic library with 'plugin' command. No linking with any libraries are required. However, to interoperate with 3proxy dynamic library must have an export function 3proxy may call to pass the structure with required information.</p>
<pre><code>typedef int (*PLUGINFUNC) (struct pluginlink *pluginlink, int argc, char** argv);</code></pre>
<p><code>struct pluginlink</code> is a structure with export information, explained later, argc and argv are argument counter and array of arguments of "plugin" command. Plugin should report it's status with integer return value. 0 is success, positive value indicates non-recoverable error, 3proxy do not parse rest of configuration and enters into termination state, negative value indicates recoverable value, 3proxy logs warning (if possible). In case of C++, all 3proxy functions/structures must be extern "C".</p>
<p>All 3proxy structures/functions descriptions are located in <b>structures.h</b></p>
</li>
<li>Understanding pluginlink structure
<p>Because there is no linking between 3proxy and plugin, all 3proxy functions and structures are passed with pluginlink structure. Pluginlink is actually a collection of pointers to 3proxy internal structures and functions. Because pluginlink is constantly extending, you should see it's definitions in <b>structures.h</b>.</p>
<p>most important are:</p>
<pre><code>struct symbol symbols;</code></pre>
<p>"symbols" is a kind of name/value export table, made as a list. It can be used by plugins to exchange information and functions between plugins, e.g. to export functions from one plugin to another, where pluginlink is useless, because it's static. It's quite simple:</p>
<pre><code>struct symbol {
struct symbol *next;
char * name;
void * value;
};</code></pre>
<p><code>name</code> – is a name of function or structure</p>
<p><code>value</code> – is it's value.</p>
<p>use <code>pluginlink->findbyname</code> function to lookup, e.g.</p>
<pre><code>anotherplugindata = pluginlink->findbyname("anotherplugindata");</code></pre>
<p>To export something from your plugin, add your structure to this list.</p>
<pre><code>struct extparam *conf;</code></pre>
<p>pointer to conf structure, it holds all current 3proxy configuration</p>
</li>
<li>How to get control within plugin
<p>There are few points you can get control for your plugin, after it's loaded with 'plugin' command.</p>
<ol class="level3">
<li>Adding configuration command processor with struct command structure
<p>A list of configuration file command, available from 3proxy.cfg is extendable. Each command is defined by <code>struct commands</code>:</p>
<pre><code>struct commands {
struct commands *next;
char * command;
int (* handler)(int argc, unsigned char ** argv);
int minargs;
int maxargs;
};</code></pre>
<p><code>struct commands *next</code> - next element in list</p>
<p><code>char * command</code> – command name</p>
<p><code>int (* handler)(int argc, unsigned char ** argv)</code> – command handler. It's called than 'command' is found in configuration files, argc is a number of arguments, counting command itself, argv is array of arguments.</p>
<p><code>minargs</code> – minimum number of arguments command support (>= 1)</p>
<p><code>maxargs</code> – maximum number of arguments command support, 0 means infinity.</p>
<p>Handler return value of 0 indicates command is successfully processed. Positive return value indicates non-recoverable error, 3proxy enters termination state. Negative value indicates 3proxy to continue to process command list, it makes it possible to set few handlers for the same command.</p>
<p>A list of the command is pointed by <code>pluginlink->commandhandlers</code>; you must insert you command after first one (do not replace <code>pluginlink->commandhandlers</code>). It's guaranteed at least 1 dummy command is always present.</p>
<p>Example:</p>
<pre><code>int mycommandhandler(int argc, unsigned char **argv);
struct commands mycommand;
mycommand.command = "mycommand";
mycommand.handler = mycommandhandler;
mycommand.intargs = 1;
mycommand.intargs = 2;
mycommand.next = pluginslinks->commandhandlers->next;
pluginslink->commandhandlers->next = &mycommand;</code></pre>
<p>Adds processor for "mycommand" command with zero on one arguments.</p>
<p>Adding configuration command is useful, if your plugin expects configuration data.</p>
</li>
<li>Adding authentication method with struct auth
<p>3proxy supports authentication and authorization. Authentication process determines user account (for example by username and password), authorization checks, if user account has a right to access given resource and optionally establishes a connection, if required.</p>
<p>'auth' command combines both authentication and authorization method. It's extandable with struct auth list:</p>
<pre><code>struct auth {
struct auth *next;
AUTHFUNC authenticate;
AUTHFUNC authorize;
char * desc;
};</code></pre>
<p><code>char * desc</code> – name of authentication/authorization method</p>
<p><code>authenticate</code> – name of authentication function</p>
<p><code>authorize</code> – name of authorization function</p>
<p><code>pluginlink->authfuncs</code> points to list of authenticataction structures. Like above, new structure must be inserted after fiest one (or to the end of the list).</p>
<p>First, authentication is called, if authentication indicates OK status (return value 0), authorization is called. Normally, 'checkACL' (<code>pluginlink->checkACL</code>) is called as authorization function to check user's request matches to standard allow/deny rules. If for some reason you need to avoid this check, you should call <code>pluginlink->alwaysauth</code> to do some dirty job, like establishing outgoing connection.</p>
<pre><code>typedef int (*AUTHFUNC)(struct clientparam * param);</code></pre>
<p>is both authentication and authorization function. <code>struct clientparam</code> holds all information about client connection, including username (<code>param->username</code>) and password (<code>param->password</code>).</p>
<p>Return value of 0 indicates successful authentication/authorization, 1 and 3 – authorization failed (access denied), use 3 in case you want to indicate access is explicitly denied and 3 in case there is no matching rule. 4,5,6,7,8 – authentication failed (e.g. username/password do not match). 4 indicates username does not present in request and must be requested, if possible. 5 indicates username found in request can not be found in user's database/list, 6,7,8 – username does not match password for different authentication types. 10 – user exceeded some limits, e.g. traffic. You may use some different code to indicate internal problems.</p>
<p>Example:</p>
<pre><code>int myauthfunc(struct clientparam *param);
struct auth myauth;
myauth.desc = "myauth";
myauth.authenticate = myauthfunc;
myauth.authorize = pluginlink->checkACL;
myauth->next = pluginlink->authfuncs->next;
pluginlink->authfuncs->next = &myauth;</code></pre>
<p>Installs "myauthfunc" as authentication function. There is no need to add 'auth' command processor for new authentication type, it's processed by standard 'auth' command processor.</p>
</li>
<li>Adding scheduled functions
<p>Scheduled functions are described by this structure:</p>
<pre><code>typedef enum {NONE, MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, ANNUALLY, NEVER} ROTATION;
struct schedule {
struct schedule *next;
ROTATION type;
void *data;
int (*function)(void *);
time_t start_time;
};</code></pre>
<p><code>int (*function)(void *)</code> – scheduled function</p>
<p><code>void *data</code> – this pointer will be passed as an argument to scheduled functions</p>
<p><code>ROTATION type</code> – defines how often function is called (once in a minute, hour, etc).</p>
<p><code>start_time</code> – time to begin using of scheduled function</p>
<p>Scheduled functions are called every 'type' interval after start_time and also on reloading configuration and going to termination state.</p>
<p>Schedule function return value of 1 means function must be removed from the schedule. 3proxy doesn't free struct schedule.</p>
<p>Schedule list can be empty. Pointer to schedule is pointed by <code>struct schedule ** schedule;</code> in pluginlink.</p>
<p>Example:</p>
<pre><code>int myschedfunc(void * data);
struct schedule myschedule;
myschedule.data = "somethinghere";
myschedule.function = myschedfunc;
myschedule.type = MINUTELY;
myschedule.starttime = 0;
myschedule.next = *pluginlink->schedule;
*pluginlink->schedule = myschedule;</code></pre>
<p><span class="s1">NOTE:</span> time_t is different for different compilers. Make sure to compile plugin and 3proxy with same compiler.</p>
</li>
<li>Filters API
<p>3proxy has filters API, you can use, to process client request and data flowing through proxy. It should be noted, that currently 3proxy doesn't provide filters with any useful data conversion, so, it's filter's task to find data in data flow. In case filter modifies some data, it's filter's task again to assure that everything's fine. If you know some filtering API like MILTER, you will find 3proxy filters very same.</p>
<pre><code>typedef enum {
PASS,
CONTINUE,
HANDLED,
REJECT,
REMOVE
} FILTER_ACTION;
typedef void* FILTER_OPEN(void * idata, struct srvparam * param);
typedef FILTER_ACTION FILTER_CLIENT(void *fo, struct clientparam * param, void** fc);
typedef FILTER_ACTION FILTER_BUFFER(void *fc, struct clientparam * param, unsigned char ** buf_p, int * bufsize_p, int offset, int * length_p);
typedef void FILTER_CLOSE(void *fo);
struct filter {
struct filter * next;
char * instance;
void * data;
FILTER_OPEN *filter_open;
FILTER_CLIENT *filter_client;
FILTER_BUFFER *filter_request;
FILTER_BUFFER *filter_header_cli;
FILTER_BUFFER *filter_header_srv;
FILTER_BUFFER *filter_data_cli;
FILTER_BUFFER *filter_data_srv;
FILTER_CLOSE *filter_clear;
FILTER_CLOSE *filter_close;
};</code></pre>
<p><code>char * instance</code> – is some instance identifier. You can use it to find required filter in the list. 3proxy itself doesn't use this field.</p>
<p><code>void * data</code> – this parameter is passed to filter_open function. The rest are filtering functions. Section 1 explains where and then each filter is called. data should not be NULL.</p>
<p><code>filter_open</code> must always be defined, if you want filter to be ever used. It's called then new service is created and is given "data" from struct filter and struct srvparam (parsed service configuration). If filter_open for some filter returns NULL, filter will not be used for this service. Non-NULL return value will be used as "fo" parameter for every call to filter_client.</p>
<p><code>filter_client</code> is called upon client connect (before any data is sent/received). It's good place to filter client by IP (and is not good place to filter it by hostname, because this operation takes a long time, 3proxy will not be able to accept new connection). fo is a data pointer received from filter_open, param – newly created clientparam structure, fc is return parameter filter_open must initialize, it will be used as an argument to FILTER_BUFFER functions. PASS return value means this filter will be used for this client request. CONTINUE says to install filters. On different values client connection is closed and no client thread is created.</p>
<p><code>filter_request, filter_header_cli, filter_header_srv, filter_data_cli, filter_data_srv</code> are used to process request and data received from client and server.</p>
<p><code>char ** buf_p</code> is a pointer to current buffer, <code>int * bufsize_p</code> is a pointer to it's size. In case you change some data and it doesn't fit to current buffer, you may allocate new buffer (with <code>pluginlink->myalloc</code>), copy data from old buffer, free old buffer (with <code>pluginlink->myfree</code>) and set new values for <code>*buf_p</code> and <code>*bufsize_p</code>.</p>
<p><code>int offset</code> offset of the new data in the buffer, <code>int * length_p</code> length of all data in the buffer. You should filter only <code>(*length_p - *offset_p)</code> characters starting from <code>(*buf_p + *offset_p)</code>.</p>
<p><code>filter_clear</code> is called for each successfule filter_client and should be used to free allocated resources</p>
<p><code>filter_close</code> is called for each successful filter_open for the same reason</p>
<p>An example of filter API usage you can find in PCREPlugin (see plugins/PCREPlugin/pcre_plugin.c).</p>
<p><span class="s1">Note</span>: if <code>param->nooverwritefilter</code> is set for FILTER_BUFFER functions, filter may change data in the buffer, but must not change data length. This flag may be set, if data size if already known and is sent to the client.</p>
</li>
<li>Replacing log functions, traffic counting functions, bandwidth limitation functions
<p>Log, traffic and bandwidth function can be directly replaced in any over place. All functions may be replaced in conf (<code>pluginlink->conf->logfunc</code>, <code>pluginlink->conf->bandlimfunc</code>, <code>pluginlink->conf->trafcountfunc</code>). In this case, these functions will be used for services started after the changes are made. logfunc may also be changed for struct srvparam (e.g. within filter_open), bandlimfunc and trafcountfunc may be changed in struct clientparam for every client individually (e.g. within filter_client).</p>
<pre><code>typedef void (*LOGFUNC)(struct clientparam * param, const unsigned char * test);
typedef void (*TRAFCOUNTFUNC)(struct clientparam * param);
typedef unsigned (*BANDLIMFUNC)(struct clientparam * param, unsigned nbytesin, unsigned nbytesout);</code></pre>
<p><code>struct clientparam * param</code> – information about client request</p>
<p><code>char * text</code> - text string (e.g. request)</p>
<p><code>nbytesin, nbytesout</code> – number of bytes received from / send to server. bandlimfunc returns delay in milliseconds.</p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
</body>
</html>
<p><img src="/favicon-1.png" align="right">
</div>
<br><br><br><br><br>
<script type="text/javascript">
$(document).ready(function() {
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
var offset = $(':target').offset();
var scrollto = offset.top - 60; // minus fixed header height
$('html, body').animate({scrollTop:scrollto}, 0);
});
</script>
<footer class="footer bg-dark">
<div class="container">
<span class="text-muted">© 2000-2026 3APA3A, Vladimir Dubrovin. Please contact <a class="tiny" href="mailto:3proxy@3proxy.ru">3proxy development team</a> if you want to contribute code or maintain port.
<br> design saw-friendship 2018</span>
</div>
</footer>
</body>
</html>