├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
├── CMakeLists.txt
├── LICENSE
├── README.md
├── docs
└── Template.md
├── include
├── debug.h
├── file_watcher.h
├── html_serve.h
├── request_handler.h
├── server.h
├── socket_utils.h
├── sqlite_handler.h
├── template.h
└── websocket.h
├── src
├── file_watcher.c
├── handle_client.c
├── html_serve.c
├── request_handler.c
├── server.c
├── socket_utils.c
├── sqlite_handler.c
├── template.c
└── websocket.c
├── template-examples
├── 01-variables.html
├── 02-conditionals.html
├── 03-loops.html
├── 04-conditional-loops.html
└── 05-sqlite-queries.html
├── test
├── create_sample_db.sql
├── minimal.html
├── sample.db
└── test.html
└── www
├── conditional-loops.html
├── conditionals.html
├── index.html
├── loops.html
├── sample.db
├── sql.html
└── variables.html
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .vscode
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # CMakeLists.txt
2 | cmake_minimum_required(VERSION 3.10)
3 | project(Blink)
4 |
5 | # Set output directories for binaries and libraries
6 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
7 |
8 | # Find required packages
9 | find_package(OpenSSL REQUIRED)
10 | find_package(SQLite3 REQUIRED)
11 |
12 | # Include directories
13 | include_directories(include ${OPENSSL_INCLUDE_DIR} ${SQLite3_INCLUDE_DIRS})
14 |
15 | # Add the executable
16 | add_executable(blink
17 | src/server.c
18 | src/socket_utils.c
19 | src/html_serve.c
20 | src/request_handler.c
21 | src/template.c
22 | src/file_watcher.c
23 | src/websocket.c
24 | src/sqlite_handler.c
25 | )
26 |
27 | # Link with required libraries
28 | target_link_libraries(blink ${OPENSSL_LIBRARIES} ${SQLite3_LIBRARIES} pthread)
29 |
30 | # Copy www directory to build directory
31 | add_custom_command(
32 | TARGET blink POST_BUILD
33 | COMMAND ${CMAKE_COMMAND} -E copy_directory
34 | ${CMAKE_SOURCE_DIR}/www ${CMAKE_BINARY_DIR}/bin/www
35 | COMMENT "Copying www directory to build directory"
36 | )
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 dexter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blink: Lightweight Web Server with Advanced Templating
2 |
3 | Blink is a lightweight, powerful web server written in C that features a comprehensive templating system with support for dynamic content, conditional logic, loops, and SQLite database integration. It's designed to be fast, easy to use, and perfect for both development and small-scale deployments.
4 |
5 | [](https://buymeacoffee.com/trish07)
6 |
7 | ## Features
8 |
9 | - **Lightweight HTTP Server**: Fast and efficient C-based HTTP server with minimal dependencies
10 | - **Hot Reloading**: Automatic browser refresh when HTML files are modified
11 | - **WebSocket Support**: Real-time bidirectional communication
12 | - **Comprehensive Templating System**:
13 | - Variable replacement
14 | - Conditional logic (if/else blocks)
15 | - Loops with item iteration
16 | - Conditional loops with filtering
17 | - Nested template structures
18 | - **SQLite Integration**:
19 | - Execute SQL queries directly in templates
20 | - Display query results as formatted HTML tables
21 | - Form-based database operations (Create, Read, Update, Delete)
22 | - Placeholder substitution for safe user input handling
23 | - **Flexible Configuration**:
24 | - Customizable port settings
25 | - Custom HTML file serving
26 | - Database path configuration
27 | - Template processing toggles
28 |
29 | ## Project Structure
30 |
31 | ```
32 | blink/
33 | ├── CMakeLists.txt # Build configuration
34 | ├── LICENSE # Project license
35 | ├── README.md # Project documentation
36 | ├── .gitignore # Git ignore file
37 | │
38 | ├── include/ # Header files
39 | │ ├── blink_orm.h # ORM functionality for SQLite
40 | │ ├── debug.h # Debugging utilities
41 | │ ├── file_watcher.h # File watching for hot reload
42 | │ ├── html_serve.h # HTML serving functionality
43 | │ ├── request_handler.h # HTTP request handler
44 | │ ├── server.h # Main server header
45 | │ ├── socket_utils.h # Socket utilities
46 | │ ├── sqlite_handler.h # SQLite database integration
47 | │ ├── template.h # Template processing
48 | │ └── websocket.h # WebSocket protocol support
49 | │
50 | ├── src/ # Source code files
51 | │ ├── file_watcher.c # Implementation of file watcher
52 | │ ├── handle_client.c # Client connection handler
53 | │ ├── html_serve.c # HTML content serving
54 | │ ├── request_handler.c # HTTP request processing
55 | │ ├── server.c # Main server implementation
56 | │ ├── socket_utils.c # Socket utility functions
57 | │ ├── sqlite_handler.c # SQLite database functions
58 | │ ├── template.c # Template engine implementation
59 | │ └── websocket.c # WebSocket implementation
60 | │
61 | └── build/ # Build directory (generated)
62 | └── bin/ # Compiled binaries
63 | └── blink # Main executable
64 | ```
65 |
66 | ## Quick Start
67 |
68 | ### Prerequisites
69 |
70 | - **CMake** (version 3.10 or higher)
71 | - **GCC** or another compatible C compiler
72 | - **OpenSSL** development libraries
73 | - **SQLite3** development libraries
74 | - **Linux** or **WSL** (Windows Subsystem for Linux) recommended
75 |
76 | ### Installation
77 |
78 | ```bash
79 | # Install dependencies (Debian/Ubuntu)
80 | sudo apt update
81 | sudo apt install build-essential cmake libssl-dev libsqlite3-dev
82 |
83 | # Clone the repository
84 | git clone https://github.com/dexter-xD/blink.git
85 | cd blink
86 |
87 | # Build the project
88 | mkdir build && cd build
89 | cmake ..
90 | make
91 |
92 | # Run the server
93 | ./bin/blink
94 | ```
95 |
96 | ### Command-Line Options
97 |
98 | ```
99 | Options:
100 | -p, --port PORT Specify port number (default: 8080)
101 | -s, --serve FILE Specify a custom HTML file to serve
102 | -db, --database FILE Specify SQLite database path
103 | -n, --no-templates Disable template processing
104 | -h, --help Display help message
105 | ```
106 |
107 | Example usage:
108 |
109 | ```bash
110 | # Run with a custom HTML file and SQLite database
111 | ./bin/blink --serve myapp.html --database mydata.db --port 9000
112 | ```
113 |
114 | ## Template Engine Guide
115 |
116 | The Blink template engine allows dynamic HTML generation with various powerful features. Here's an overview of the main capabilities:
117 |
118 | ### 1. Variable Replacement
119 |
120 | Define variables using HTML comments and reference them with double curly braces:
121 |
122 | ```html
123 |
124 |
125 |
Welcome, {{username}}!
126 | You are logged into {{company}} systems.
127 | ```
128 |
129 | ### 2. Conditional Logic
130 |
131 | Use if-else blocks to display content conditionally:
132 |
133 | ```html
134 |
135 |
136 | {% if is_admin %}
137 |
138 |
Admin Controls
139 |
140 |
141 | {% else %}
142 | You don't have admin privileges.
143 | {% endif %}
144 | ```
145 |
146 | ### 3. Loops
147 |
148 | Iterate over items using for loops:
149 |
150 | ```html
151 |
152 |
153 |
154 | {% for item in items %}
155 | {{item}}
156 | {% endfor %}
157 |
158 | ```
159 |
160 | ### 4. Multi-part Items
161 |
162 | Use pipe-delimited values for structured data:
163 |
164 | ```html
165 |
166 |
167 |
168 | Product Category Price
169 | {% for item in items %}
170 |
171 | {{item.0}}
172 | {{item.1}}
173 | ${{item.2}}
174 |
175 | {% endfor %}
176 |
177 | ```
178 |
179 | ### 5. Conditional Loops
180 |
181 | Filter items in loops using conditions:
182 |
183 | ```html
184 |
185 |
186 | Fruits Only:
187 |
188 | {% for item in items if item.1 == "Fruit" %}
189 | {{item.0}} - ${{item.2}}
190 | {% endfor %}
191 |
192 | ```
193 |
194 | ### 6. SQLite Integration
195 |
196 | Execute SQL queries directly in your templates:
197 |
198 | ```html
199 | User List
200 | {% query "SELECT id, name, email FROM users ORDER BY name LIMIT 10" %}
201 |
202 | Item Statistics
203 | {% query "SELECT category, COUNT(*) as count, AVG(price) as avg_price FROM products GROUP BY category" %}
204 | ```
205 |
206 | ### 7. Form-Based Database Operations
207 |
208 | Create forms that perform database operations:
209 |
210 | ```html
211 |
224 | ```
225 |
226 | For detailed documentation on all template features, see the [Template Documentation](./docs/Template.md).
227 |
228 | ## Example Applications
229 |
230 | ### 1. Simple Dynamic Page
231 |
232 | ```html
233 |
234 |
235 |
236 |
237 |
238 | {{page_title}}
239 |
240 |
241 | Hello, {{username}}!
242 | Welcome to our website.
243 |
244 |
245 | ```
246 |
247 | ### 2. Data Dashboard with SQLite
248 |
249 | ```html
250 |
251 |
252 |
253 | Sales Dashboard
254 |
255 |
256 | Sales Dashboard
257 |
258 | Recent Orders
259 | {% query "SELECT id, customer_name, amount, date FROM orders ORDER BY date DESC LIMIT 5" %}
260 |
261 | Sales by Category
262 | {% query "SELECT category, SUM(amount) as total FROM orders GROUP BY category ORDER BY total DESC" %}
263 |
264 |
265 | ```
266 |
267 | ## Hot Reload Feature
268 |
269 | Blink's hot reload feature automatically refreshes connected browsers when HTML files are modified:
270 |
271 | 1. Start the server with default options
272 | 2. Edit any HTML file in your project directory
273 | 3. The browser will automatically refresh to show your changes
274 |
275 | This feature works by:
276 | - Watching file system events in the HTML directory
277 | - Using WebSockets to notify connected clients
278 | - Injecting a small JavaScript snippet into served HTML pages
279 |
280 | ## WebSocket Support
281 |
282 | Blink includes WebSocket support for real-time bidirectional communication:
283 |
284 | 1. Access WebSocket functionality at `/ws` endpoint
285 | 2. Establish a WebSocket connection from your client-side JavaScript
286 | 3. Exchange messages between client and server in real time
287 |
288 | ## SQLite Database Integration
289 |
290 | To use SQLite features:
291 |
292 | 1. Start Blink with a database: `./bin/blink --database mydata.db`
293 | 2. Use `{% query "SQL_STATEMENT" %}` tags in your HTML templates
294 | 3. Create forms with action="/sql" to perform database operations
295 |
296 | ## Contributing
297 |
298 | Contributions are welcome! Please feel free to submit a Pull Request.
299 |
300 | ## Support Development
301 |
302 | If you find Blink useful, consider supporting its development:
303 |
304 | [](https://www.buymeacoffee.com/yourusername)
305 |
306 | ## License
307 |
308 | This project is licensed under the MIT License - see the LICENSE file for details.
309 |
310 | ---
311 |
312 | ## Template Engine Documentation
313 |
314 | For full documentation on the template engine, see [docs/Template.md](./docs/Template.md).
--------------------------------------------------------------------------------
/docs/Template.md:
--------------------------------------------------------------------------------
1 | # Blink Template Engine Documentation
2 |
3 | This document provides a comprehensive guide to using the Blink template engine for dynamic HTML content generation.
4 |
5 | ## Table of Contents
6 |
7 | 1. [Introduction](#introduction)
8 | 2. [Basic Template Syntax](#basic-template-syntax)
9 | 3. [Template Variables](#template-variables)
10 | 4. [Conditional Logic](#conditional-logic)
11 | 5. [Loops](#loops)
12 | 6. [Conditional Loops](#conditional-loops)
13 | 7. [SQLite Queries](#sqlite-queries)
14 | 8. [Nested Elements](#nested-elements)
15 | 9. [Best Practices](#best-practices)
16 |
17 | ## Introduction
18 |
19 | The Blink template engine allows for dynamic HTML generation by combining static HTML with dynamic content. It supports:
20 |
21 | - Variable replacement
22 | - Conditional blocks (if/else)
23 | - Loops over items
24 | - Conditional loops with filtering
25 | - SQLite database queries
26 | - Part extraction from delimited values
27 |
28 | ## Basic Template Syntax
29 |
30 | ### Variable Placeholders
31 |
32 | Variables are referenced using double curly braces:
33 |
34 | ```html
35 | Hello, {{username}}!
36 | ```
37 |
38 | ### Conditionals
39 |
40 | Conditional blocks use the following syntax:
41 |
42 | ```html
43 | {% if condition %}
44 |
45 | {% else %}
46 |
47 | {% endif %}
48 | ```
49 |
50 | ### Loops
51 |
52 | Loops use the following syntax:
53 |
54 | ```html
55 | {% for item in items %}
56 |
57 | {% endfor %}
58 | ```
59 |
60 | ### SQLite Queries
61 |
62 | SQL queries use the following syntax:
63 |
64 | ```html
65 | {% query "SELECT column1, column2 FROM table_name WHERE condition" %}
66 | ```
67 |
68 | ### Conditional Loops
69 |
70 | Loops with conditions use the following syntax:
71 |
72 | ```html
73 | {% for item in items if item.1 == "value" %}
74 |
75 | {% endfor %}
76 | ```
77 |
78 | ## Template Variables
79 |
80 | ### Defining Variables
81 |
82 | Variables can be defined in HTML comments:
83 |
84 | ```html
85 |
86 | ```
87 |
88 | ### Using Variables
89 |
90 | Variables are accessed using double curly braces:
91 |
92 | ```html
93 | {{page_title}}
94 | Welcome, {{username}}!
95 | ```
96 |
97 | ### Multi-part Variables
98 |
99 | Variables can contain delimited values (using the pipe character):
100 |
101 | ```html
102 |
103 | ```
104 |
105 | **Note**: Accessing parts of regular variables directly (like `{{product.0}}`) is not supported. Use loops for part extraction.
106 |
107 | ## Conditional Logic
108 |
109 | ### Basic Conditions
110 |
111 | ```html
112 | {% if is_admin %}
113 | Admin Panel
114 | {% endif %}
115 | ```
116 |
117 | ### If/Else Blocks
118 |
119 | ```html
120 | {% if is_logged_in %}
121 | Welcome back, {{username}}!
122 | {% else %}
123 | Please log in to continue.
124 | {% endif %}
125 | ```
126 |
127 | ### Condition Values
128 |
129 | The engine treats the following values as "true":
130 | - "1", "true", "yes", "y", "on"
131 | - Any non-empty string not explicitly false
132 |
133 | The following values are treated as "false":
134 | - "0", "false", "no", "n", "off"
135 | - Empty strings
136 |
137 | ## Loops
138 |
139 | ### Defining Loop Items
140 |
141 | Loop items are defined in HTML comments:
142 |
143 | ```html
144 |
145 | ```
146 |
147 | ### Basic Loop
148 |
149 | ```html
150 |
151 | {% for item in items %}
152 | {{item}}
153 | {% endfor %}
154 |
155 | ```
156 |
157 | ### Multi-part Items
158 |
159 | Items can contain parts separated by the pipe character:
160 |
161 | ```html
162 |
163 | ```
164 |
165 | Access parts using dot notation:
166 |
167 | ```html
168 |
169 | {% for item in items %}
170 | {{item.0}} - {{item.1}} - ${{item.3}}
171 | {% endfor %}
172 |
173 | ```
174 |
175 | ## Conditional Loops
176 |
177 | ### Filtering by Exact Match
178 |
179 | ```html
180 |
181 | {% for item in items if item.1 == "fruit" %}
182 | {{item.0}} - ${{item.3}}
183 | {% endfor %}
184 |
185 | ```
186 |
187 | ### Filtering by Inequality
188 |
189 | ```html
190 |
191 | {% for item in items if item.1 != "vegetable" %}
192 | {{item.0}} - ${{item.3}}
193 | {% endfor %}
194 |
195 | ```
196 |
197 | ### Multiple Conditions
198 |
199 | Currently only single conditions are supported. For complex filtering, pre-filter your data.
200 |
201 | ## SQLite Queries
202 |
203 | ### Setting Up SQLite
204 |
205 | To use SQLite queries in your templates, you must start the server with a valid SQLite database using the `-db` or `--database` command-line option:
206 |
207 | ```bash
208 | ./bin/blink --database path/to/your/database.db
209 | ```
210 |
211 | If you don't provide a database path, but still want to use SQLite features, Blink will create a default database in the current directory named `blink.db`.
212 |
213 | ### Basic Query Syntax
214 |
215 | Execute SQL queries directly in your templates using the query tag:
216 |
217 | ```html
218 | {% query "SELECT * FROM users LIMIT 10" %}
219 | ```
220 |
221 | The query results will be automatically rendered as an HTML table with the class `sql-table`.
222 |
223 | ### Query Examples
224 |
225 | #### Basic Queries
226 |
227 | Query all tables in the database:
228 |
229 | ```html
230 | {% query "SELECT name, type FROM sqlite_master WHERE type='table'" %}
231 | ```
232 |
233 | Query specific data with conditions:
234 |
235 | ```html
236 | {% query "SELECT id, name, email FROM users WHERE status = 'active'" %}
237 | ```
238 |
239 | #### Advanced Queries
240 |
241 | Perform joins across tables:
242 |
243 | ```html
244 | {% query "SELECT o.id, u.name, o.total FROM orders o JOIN users u ON o.user_id = u.id" %}
245 | ```
246 |
247 | Aggregate functions:
248 |
249 | ```html
250 | {% query "SELECT COUNT(*) AS total, AVG(price) AS average FROM products" %}
251 | ```
252 |
253 | Grouping and ordering:
254 |
255 | ```html
256 | {% query "SELECT category, COUNT(*) AS count FROM products GROUP BY category ORDER BY count DESC" %}
257 | ```
258 |
259 | ### Form-Based Database Operations
260 |
261 | Blink supports form-based database operations through POST requests to the `/sql` endpoint.
262 |
263 | #### Creating Tables
264 |
265 | ```html
266 |
276 | ```
277 |
278 | #### Inserting Data
279 |
280 | ```html
281 |
282 |
283 |
284 | Name:
285 |
286 |
287 | Email:
288 |
289 |
290 |
291 |
292 | Add User
293 |
294 | ```
295 |
296 | #### Updating Data
297 |
298 | ```html
299 |
300 |
301 |
302 | User ID:
303 |
304 |
305 | New Name:
306 |
307 |
308 |
309 |
310 | Update User
311 |
312 | ```
313 |
314 | #### Deleting Data
315 |
316 | ```html
317 |
318 |
319 |
320 | User ID to Delete:
321 |
322 |
323 |
324 |
325 | Delete User
326 |
327 | ```
328 |
329 | ### Placeholder Substitution
330 |
331 | In SQL forms, you can use placeholders surrounded by square brackets `[placeholder]` to be replaced with form field values:
332 |
333 | ```html
334 | INSERT INTO users (name, email, age) VALUES ('[name]', '[email]', [age])
335 | ```
336 |
337 | Note that for numeric values (like `[age]`), the brackets don't include quotes, allowing the value to be treated as a number.
338 |
339 | ### Error Handling
340 |
341 | If a query fails (e.g., syntax error, nonexistent table), an error message will be displayed. For successful queries:
342 |
343 | - SELECT queries will show results in a formatted table
344 | - INSERT, UPDATE, DELETE will show success message with affected row count
345 | - CREATE, DROP, etc. will show success message
346 |
347 | ## Nested Elements
348 |
349 | The template engine supports nesting conditional blocks and loops.
350 |
351 | ### Nested Conditionals
352 |
353 | ```html
354 | {% if is_logged_in %}
355 |
356 | {% if is_admin %}
357 |
Admin Panel
358 | {% else %}
359 |
Welcome, standard user!
360 | {% endif %}
361 |
362 | {% endif %}
363 | ```
364 |
365 | ### Nested Loops
366 |
367 | ```html
368 | {% for category in categories %}
369 | {{category.0}}
370 |
371 | {% for product in products if product.1 == category.0 %}
372 | {{product.0}} - ${{product.2}}
373 | {% endfor %}
374 |
375 | {% endfor %}
376 | ```
377 |
378 | ## Best Practices
379 |
380 | ### Performance Considerations
381 |
382 | - Keep SQL queries simple and optimized
383 | - Avoid excessive nesting of conditionals and loops
384 | - Use specific column names in SELECT queries rather than *
385 | - Add LIMIT clauses to queries when appropriate
386 |
387 | ### Template Organization
388 |
389 | - Use clear, descriptive variable names
390 | - Comment your templates for clarity
391 | - Break complex templates into smaller, reusable parts
392 | - Organize your data efficiently with pipe-delimited values
393 |
394 | ### Security
395 |
396 | - Never include sensitive information in template comments
397 | - Avoid using direct user input in SQL queries
398 | - Validate all form input on the server
399 | - Use appropriate data types for SQL fields (e.g., INTEGER for numbers)
400 |
401 | ### Debugging
402 |
403 | - Check server logs for SQL errors
404 | - Verify database connection when template features aren't working
405 | - Test templates with sample data before using in production
406 | - Validate HTML output for proper structure
407 |
408 | ## Full Example
409 |
410 | Here's a complete example combining multiple template features:
411 |
412 | ```html
413 |
414 |
415 |
416 | {{page_title}}
417 |
422 |
423 |
424 |
425 |
431 |
432 | {{page_title}}
433 |
434 | {% if is_admin %}
435 |
436 |
Admin Controls
437 |
Welcome, Administrator!
438 |
439 | {% endif %}
440 |
441 | Product Categories
442 |
443 | {% for category in items %}
444 |
445 | {{category.0}} : {{category.1}}
446 | ({{category.2}} products)
447 |
448 | {% endfor %}
449 |
450 |
451 | High-Stock Categories
452 |
453 | {% for category in items if category.2 > 20 %}
454 | {{category.0}} ({{category.2}} products)
455 | {% endfor %}
456 |
457 |
458 | Recent Products
459 | {% query "SELECT id, name, price, category FROM products ORDER BY id DESC LIMIT 5" %}
460 |
461 | Add New Product
462 |
463 |
464 |
465 | Product Name:
466 |
467 |
468 | Price:
469 |
470 |
471 | Category:
472 |
473 | {% for cat in items %}
474 | {{cat.0}}
475 | {% endfor %}
476 |
477 |
478 |
479 |
480 | Add Product
481 |
482 |
483 |
484 | ```
--------------------------------------------------------------------------------
/include/debug.h:
--------------------------------------------------------------------------------
1 | #ifndef DEBUG_H
2 | #define DEBUG_H
3 |
4 | // Uncomment the line below to enable debug messages
5 | // #define DEBUG_MODE
6 |
7 | #endif /* DEBUG_H */
--------------------------------------------------------------------------------
/include/file_watcher.h:
--------------------------------------------------------------------------------
1 | #ifndef FILE_WATCHER_H
2 | #define FILE_WATCHER_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | #define EVENT_SIZE (sizeof(struct inotify_event))
10 | #define BUF_LEN (1024 * (EVENT_SIZE + 16))
11 |
12 | typedef struct {
13 | char* path;
14 | time_t last_modified;
15 | } file_info_t;
16 |
17 | typedef struct {
18 | char* directory;
19 | bool* file_changed;
20 | pthread_mutex_t* mutex;
21 | } watcher_args_t;
22 |
23 | int init_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex);
24 | pthread_t start_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex);
25 | bool file_has_changed(const char* filename, time_t* last_modified);
26 | void* watch_files(void* args);
27 |
28 | #endif
--------------------------------------------------------------------------------
/include/html_serve.h:
--------------------------------------------------------------------------------
1 | #ifndef HTML_SERVE_H
2 | #define HTML_SERVE_H
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | char* serve_html(const char* filename);
9 | char* inject_hot_reload_js(char* html_content);
10 |
11 | #endif
12 |
--------------------------------------------------------------------------------
/include/request_handler.h:
--------------------------------------------------------------------------------
1 | #ifndef REQUEST_HANDLER_H
2 | #define REQUEST_HANDLER_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include "html_serve.h"
9 | #include "template.h"
10 | #include "websocket.h"
11 | #include "server.h"
12 | #include "sqlite_handler.h"
13 |
14 | #define BUFFER_SIZE 1024
15 |
16 | extern ws_clients_t* ws_clients;
17 | extern bool enable_templates;
18 |
19 | void handle_client(int new_socket);
20 | void handle_websocket_client(int new_socket, ws_clients_t* clients);
21 | int is_websocket_request(const char* buffer);
22 | bool has_template_features(const char* content);
23 | void set_template_settings(bool enabled);
24 | void set_custom_html_file(const char* file_path);
25 | void set_server_port(int port);
26 |
27 | #endif
28 |
--------------------------------------------------------------------------------
/include/server.h:
--------------------------------------------------------------------------------
1 | #ifndef SERVER_H
2 | #define SERVER_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include "socket_utils.h"
12 | #include "request_handler.h"
13 | #include "file_watcher.h"
14 | #include "websocket.h"
15 |
16 | #define PORT 8080
17 | #define BUFFER_SIZE 1024
18 | #define HTML_DIR "../www"
19 |
20 | #endif
21 |
--------------------------------------------------------------------------------
/include/socket_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef SOCKET_UTILS_H
2 | #define SOCKET_UTILS_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #define PORT 8080
13 | #define BUFFER_SIZE 1024
14 |
15 | int initialize_server(struct sockaddr_in* address);
16 | void read_client_data(int socket, char* buffer);
17 |
18 | #endif
19 |
--------------------------------------------------------------------------------
/include/sqlite_handler.h:
--------------------------------------------------------------------------------
1 | #ifndef SQLITE_HANDLER_H
2 | #define SQLITE_HANDLER_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | typedef struct {
11 | char*** rows;
12 | char** columns;
13 | int row_count;
14 | int column_count;
15 | int capacity;
16 | } sqlite_result_t;
17 |
18 | int init_sqlite(const char* db_path);
19 | void close_sqlite();
20 | sqlite_result_t* execute_query(const char* query);
21 | void free_query_results(sqlite_result_t* results);
22 | bool is_db_initialized();
23 | const char* get_db_path();
24 | void set_db_path(const char* path);
25 | char* process_sqlite_queries(char* content);
26 | char* generate_table_html(sqlite_result_t* result);
27 |
28 | #endif
--------------------------------------------------------------------------------
/include/template.h:
--------------------------------------------------------------------------------
1 | #ifndef TEMPLATE_H
2 | #define TEMPLATE_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | typedef struct {
11 | char** keys;
12 | char** values;
13 | int count;
14 | int capacity;
15 | } template_data_t;
16 |
17 | char* replace_placeholders(char* result, const char** keys, const char** values, int num_pairs);
18 | char* process_if_else(char* result, const char** keys, const char** values, int num_pairs);
19 | char* process_loops(char* result, const char* loop_key, const char** loop_values, int loop_count);
20 | char* process_template(const char* template, const char** keys, const char** values, int num_pairs, const char* loop_key, const char** loop_values, int loop_count);
21 | char* process_template_auto(const char* template, const char** prog_keys, const char** prog_values, int prog_pairs);
22 | char* get_item_part(const char* item, char delimiter, int part_index);
23 |
24 |
25 | template_data_t* init_template_data(void);
26 | void add_template_var(template_data_t* data, const char* key, const char* value);
27 | template_data_t* parse_template_variables(const char* template_str);
28 | void free_template_data(template_data_t* data);
29 |
30 | #endif
31 |
--------------------------------------------------------------------------------
/include/websocket.h:
--------------------------------------------------------------------------------
1 | #ifndef WEBSOCKET_H
2 | #define WEBSOCKET_H
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #define MAX_CLIENTS 50
13 | #define BUFFER_SIZE 1024
14 | #define WS_HANDSHAKE_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
15 | #define WS_RESPONSE "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n"
16 |
17 | #define WS_TEXT 0x01
18 | #define WS_BINARY 0x02
19 | #define WS_CLOSE 0x08
20 | #define WS_PING 0x09
21 | #define WS_PONG 0x0A
22 |
23 | #define COLOR_RESET "\x1B[0m"
24 | #define COLOR_RED "\x1B[31m"
25 | #define COLOR_GREEN "\x1B[32m"
26 | #define COLOR_YELLOW "\x1B[33m"
27 | #define COLOR_BLUE "\x1B[34m"
28 | #define COLOR_MAGENTA "\x1B[35m"
29 | #define COLOR_CYAN "\x1B[36m"
30 | #define COLOR_WHITE "\x1B[37m"
31 | #define BOLD "\x1B[1m"
32 | #define UNDERLINE "\x1B[4m"
33 |
34 | typedef struct {
35 | int client_sockets[MAX_CLIENTS];
36 | int count;
37 | pthread_mutex_t mutex;
38 | } ws_clients_t;
39 |
40 | ws_clients_t* init_ws_clients();
41 | void add_ws_client(ws_clients_t* clients, int socket_fd);
42 | void remove_ws_client(ws_clients_t* clients, int socket_fd);
43 | bool is_client_connected(int socket_fd);
44 | int process_ws_handshake(int client_socket, char* buffer);
45 | int send_ws_frame(int client_socket, const char* message, size_t length, int opcode);
46 | void broadcast_to_ws_clients(ws_clients_t* clients, const char* message);
47 | void handle_ws_connection(int client_socket, ws_clients_t* clients);
48 | void free_ws_clients(ws_clients_t* clients);
49 |
50 | #endif
--------------------------------------------------------------------------------
/src/file_watcher.c:
--------------------------------------------------------------------------------
1 | #include "file_watcher.h"
2 | #include "websocket.h"
3 | #include "request_handler.h"
4 |
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | typedef struct {
15 | char** filenames;
16 | time_t* last_modified;
17 | int count;
18 | int capacity;
19 | } html_files_t;
20 |
21 | static html_files_t* html_files = NULL;
22 |
23 | static html_files_t* init_html_files() {
24 | html_files_t* files = malloc(sizeof(html_files_t));
25 | if (!files) return NULL;
26 |
27 | files->capacity = 10;
28 | files->count = 0;
29 | files->filenames = malloc(files->capacity * sizeof(char*));
30 | files->last_modified = malloc(files->capacity * sizeof(time_t));
31 |
32 | if (!files->filenames || !files->last_modified) {
33 | if (files->filenames) free(files->filenames);
34 | if (files->last_modified) free(files->last_modified);
35 | free(files);
36 | return NULL;
37 | }
38 |
39 | return files;
40 | }
41 |
42 | static void add_html_file(html_files_t* files, const char* filename) {
43 | if (!files || !filename) return;
44 | for (int i = 0; i < files->count; i++) {
45 | if (strcmp(files->filenames[i], filename) == 0) {
46 | return;
47 | }
48 | }
49 |
50 | if (files->count >= files->capacity) {
51 | int new_capacity = files->capacity * 2;
52 | char** new_filenames = realloc(files->filenames, new_capacity * sizeof(char*));
53 | time_t* new_last_modified = realloc(files->last_modified, new_capacity * sizeof(time_t));
54 |
55 | if (!new_filenames || !new_last_modified) {
56 | if (new_filenames) files->filenames = new_filenames;
57 | fprintf(stderr, "%s%s[WARNING] %sMemory allocation failed, continuing with existing capacity%s\n",
58 | BOLD, COLOR_YELLOW, COLOR_RESET, COLOR_RESET);
59 | return;
60 | }
61 |
62 | files->filenames = new_filenames;
63 | files->last_modified = new_last_modified;
64 | files->capacity = new_capacity;
65 | }
66 |
67 | files->filenames[files->count] = strdup(filename);
68 | files->last_modified[files->count] = 0;
69 | files->count++;
70 |
71 | printf("%s%s[FILE WATCHER] %sAdded HTML file to watch: %s%s%s\n",
72 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, filename, COLOR_RESET);
73 | }
74 |
75 | static void scan_directory(const char* directory, html_files_t* files) {
76 | DIR* dir = opendir(directory);
77 | if (!dir) {
78 | fprintf(stderr, "%s%s[ERROR] %sFailed to open directory: %s%s\n",
79 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
80 | return;
81 | }
82 |
83 | struct dirent* entry;
84 | while ((entry = readdir(dir)) != NULL) {
85 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
86 | continue;
87 | }
88 |
89 | char* extension = strrchr(entry->d_name, '.');
90 | if (extension && strcmp(extension, ".html") == 0) {
91 | char full_path[512];
92 | snprintf(full_path, sizeof(full_path), "%s/%s", directory, entry->d_name);
93 | add_html_file(files, full_path);
94 | }
95 | }
96 |
97 | closedir(dir);
98 | }
99 |
100 | static void add_custom_html_file_if_exists(html_files_t* files) {
101 | extern char* custom_html_file;
102 |
103 | if (custom_html_file && *custom_html_file) {
104 | struct stat st;
105 | if (stat(custom_html_file, &st) == 0 && S_ISREG(st.st_mode)) {
106 | bool already_watching = false;
107 | for (int i = 0; i < files->count; i++) {
108 | if (strcmp(files->filenames[i], custom_html_file) == 0) {
109 | already_watching = true;
110 | break;
111 | }
112 | }
113 |
114 | if (!already_watching) {
115 | printf("%s%s[FILE WATCHER] %sAdding custom HTML file to watch: %s%s%s\n",
116 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, custom_html_file, COLOR_RESET);
117 | add_html_file(files, custom_html_file);
118 | }
119 | } else {
120 | fprintf(stderr, "%s%s[WARNING] %sCustom HTML file not found or not accessible: %s%s\n",
121 | BOLD, COLOR_YELLOW, COLOR_RESET, custom_html_file, COLOR_RESET);
122 | }
123 | }
124 | }
125 |
126 | static void free_html_files(html_files_t* files) {
127 | if (!files) return;
128 |
129 | for (int i = 0; i < files->count; i++) {
130 | free(files->filenames[i]);
131 | }
132 |
133 | free(files->filenames);
134 | free(files->last_modified);
135 | free(files);
136 | }
137 |
138 | int init_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex) {
139 | if (!directory || !file_changed || !mutex) {
140 | return -1;
141 | }
142 |
143 | struct stat st;
144 | if (stat(directory, &st) == -1) {
145 | fprintf(stderr, "%s%s[ERROR] %sError checking directory: %s%s\n",
146 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
147 | return -1;
148 | }
149 |
150 | if (!S_ISDIR(st.st_mode)) {
151 | fprintf(stderr, "%s%s[ERROR] %s%s is not a directory%s\n",
152 | BOLD, COLOR_RED, COLOR_RESET, directory, COLOR_RESET);
153 | return -1;
154 | }
155 |
156 | *file_changed = false;
157 |
158 | html_files = init_html_files();
159 | if (!html_files) {
160 | fprintf(stderr, "%s%s[ERROR] %sFailed to initialize HTML file tracking%s\n",
161 | BOLD, COLOR_RED, COLOR_RESET, COLOR_RESET);
162 | return -1;
163 | }
164 |
165 | scan_directory(directory, html_files);
166 |
167 | add_custom_html_file_if_exists(html_files);
168 |
169 | printf("%s%s[FILE WATCHER] %sInitialized for directory: %s%s%s (tracking %s%d%s HTML files)\n",
170 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, directory, COLOR_RESET,
171 | COLOR_YELLOW, html_files->count, COLOR_RESET);
172 |
173 | return 0;
174 | }
175 |
176 | pthread_t start_file_watcher(const char* directory, bool* file_changed, pthread_mutex_t* mutex) {
177 | pthread_t thread_id;
178 | watcher_args_t* args = malloc(sizeof(watcher_args_t));
179 |
180 | if (!args) {
181 | fprintf(stderr, "%s%s[ERROR] %sFailed to allocate memory for watcher args: %s%s\n",
182 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
183 | return 0;
184 | }
185 |
186 | args->directory = strdup(directory);
187 | args->file_changed = file_changed;
188 | args->mutex = mutex;
189 |
190 | if (pthread_create(&thread_id, NULL, watch_files, args) != 0) {
191 | fprintf(stderr, "%s%s[ERROR] %sFailed to create file watcher thread: %s%s\n",
192 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
193 | free(args->directory);
194 | free(args);
195 | return 0;
196 | }
197 |
198 | return thread_id;
199 | }
200 |
201 | bool file_has_changed(const char* filename, time_t* last_modified) {
202 | struct stat attr;
203 | if (stat(filename, &attr) == 0) {
204 | if (*last_modified == 0 || attr.st_mtime > *last_modified) {
205 | time_t old_time = *last_modified;
206 | *last_modified = attr.st_mtime;
207 | return (old_time != 0 && old_time != attr.st_mtime);
208 | }
209 | }
210 | return false;
211 | }
212 |
213 | static bool check_all_files_for_changes(html_files_t* files) {
214 | if (!files || files->count == 0) return false;
215 |
216 | static char* last_changed_file = NULL;
217 | static time_t last_change_time = 0;
218 | time_t current_time = time(NULL);
219 | const int SAME_FILE_DEBOUNCE_SECS = 5;
220 |
221 | bool any_changed = false;
222 | for (int i = 0; i < files->count; i++) {
223 | if (file_has_changed(files->filenames[i], &files->last_modified[i])) {
224 | bool is_repeat = (last_changed_file && strcmp(files->filenames[i], last_changed_file) == 0);
225 | bool within_debounce = (difftime(current_time, last_change_time) < SAME_FILE_DEBOUNCE_SECS);
226 |
227 | if (!is_repeat || !within_debounce) {
228 | printf("%s%s[FILE WATCHER] %sDetected change in file: %s%s%s\n",
229 | BOLD, COLOR_BLUE, COLOR_YELLOW, COLOR_CYAN,
230 | files->filenames[i], COLOR_RESET);
231 |
232 | if (last_changed_file) {
233 | free(last_changed_file);
234 | }
235 | last_changed_file = strdup(files->filenames[i]);
236 | last_change_time = current_time;
237 | }
238 |
239 | any_changed = true;
240 | }
241 | }
242 |
243 | return any_changed;
244 | }
245 |
246 | static void rescan_directory_if_needed(const char* directory, html_files_t* files) {
247 | static time_t last_scan_time = 0;
248 | time_t current_time = time(NULL);
249 |
250 | if (difftime(current_time, last_scan_time) > 30) {
251 | printf("%s%s[FILE WATCHER] %sRescanning directory for new HTML files...%s\n",
252 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET);
253 |
254 | scan_directory(directory, files);
255 | add_custom_html_file_if_exists(files);
256 |
257 | last_scan_time = current_time;
258 | }
259 | }
260 |
261 | void* watch_files(void* args) {
262 | watcher_args_t* watcher_args = (watcher_args_t*)args;
263 | int fd, wd, custom_file_wd = -1;
264 | char buffer[BUF_LEN];
265 | extern char* custom_html_file;
266 |
267 | time_t last_notification_time = 0;
268 | const int DEBOUNCE_TIME_MS = 1000;
269 | fd = inotify_init();
270 | if (fd < 0) {
271 | fprintf(stderr, "%s%s[ERROR] %sinotify_init failed: %s%s\n",
272 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
273 | free(watcher_args->directory);
274 | free(watcher_args);
275 | return NULL;
276 | }
277 |
278 | wd = inotify_add_watch(fd, watcher_args->directory,
279 | IN_MODIFY | IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB);
280 | if (wd < 0) {
281 | fprintf(stderr, "%s%s[ERROR] %sinotify_add_watch failed: %s%s\n",
282 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
283 | close(fd);
284 | free(watcher_args->directory);
285 | free(watcher_args);
286 | return NULL;
287 | }
288 |
289 | if (custom_html_file && *custom_html_file) {
290 | char* last_slash = strrchr(custom_html_file, '/');
291 | if (last_slash) {
292 | char custom_dir[512] = {0};
293 | strncpy(custom_dir, custom_html_file, last_slash - custom_html_file);
294 | custom_dir[last_slash - custom_html_file] = '\0';
295 |
296 | if (strcmp(custom_dir, watcher_args->directory) != 0) {
297 | custom_file_wd = inotify_add_watch(fd, custom_dir,
298 | IN_MODIFY | IN_CREATE | IN_CLOSE_WRITE | IN_MOVED_TO | IN_ATTRIB);
299 |
300 | if (custom_file_wd >= 0) {
301 | printf("%s%s[FILE WATCHER] %sAdded watch for custom HTML file directory: %s%s%s\n",
302 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, custom_dir, COLOR_RESET);
303 | }
304 | }
305 | }
306 | }
307 |
308 | printf("%s%s[FILE WATCHER] %sActive - watching directory: %s%s%s\n",
309 | BOLD, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN, watcher_args->directory, COLOR_RESET);
310 |
311 | while (1) {
312 | bool change_detected = false;
313 | if (check_all_files_for_changes(html_files)) {
314 | change_detected = true;
315 | }
316 |
317 | fd_set read_fds;
318 | FD_ZERO(&read_fds);
319 | FD_SET(fd, &read_fds);
320 | struct timeval tv;
321 | tv.tv_sec = 0;
322 | tv.tv_usec = 100000;
323 |
324 | int ret = select(fd + 1, &read_fds, NULL, NULL, &tv);
325 |
326 | if (ret > 0 && FD_ISSET(fd, &read_fds)) {
327 | int length = read(fd, buffer, BUF_LEN);
328 | if (length > 0) {
329 | int i = 0;
330 | while (i < length) {
331 | struct inotify_event* event = (struct inotify_event*)&buffer[i];
332 |
333 | if (event->len > 0) {
334 | char* dot = strrchr(event->name, '.');
335 | if (dot && (strcmp(dot, ".html") == 0)) {
336 | printf("%s%s[FILE WATCHER] %sEvent detected: %s%s%s (mask: 0x%08x)\n",
337 | BOLD, COLOR_BLUE, COLOR_RESET, COLOR_CYAN, event->name, COLOR_RESET, event->mask);
338 | change_detected = true;
339 | char full_path[512];
340 | snprintf(full_path, sizeof(full_path), "%s/%s",
341 | watcher_args->directory, event->name);
342 | add_html_file(html_files, full_path);
343 | }
344 | }
345 |
346 | i += EVENT_SIZE + event->len;
347 | }
348 | }
349 | } else if (ret < 0 && errno != EINTR) {
350 | fprintf(stderr, "%s%s[FILE WATCHER] %sSelect error: %s%s\n",
351 | BOLD, COLOR_RED, COLOR_RESET, strerror(errno), COLOR_RESET);
352 | }
353 |
354 | rescan_directory_if_needed(watcher_args->directory, html_files);
355 | if (change_detected) {
356 | time_t current_time = time(NULL);
357 | double ms_since_last = difftime(current_time, last_notification_time) * 1000;
358 | if (ms_since_last >= DEBOUNCE_TIME_MS) {
359 | bool already_signaled = false;
360 | pthread_mutex_lock(watcher_args->mutex);
361 | already_signaled = *(watcher_args->file_changed);
362 | pthread_mutex_unlock(watcher_args->mutex);
363 |
364 | if (!already_signaled) {
365 | usleep(100000);
366 | pthread_mutex_lock(watcher_args->mutex);
367 | *(watcher_args->file_changed) = true;
368 | pthread_mutex_unlock(watcher_args->mutex);
369 | printf("%s%s[FILE WATCHER] %sSignaled main thread about file changes%s\n",
370 | BOLD, COLOR_BLUE, COLOR_YELLOW, COLOR_RESET);
371 | last_notification_time = current_time;
372 | } else {
373 | static time_t last_skip_message = 0;
374 | if (difftime(current_time, last_skip_message) > 5) {
375 | printf("%s%s[FILE WATCHER] %sSkipping notification - one already pending%s\n",
376 | BOLD, COLOR_BLUE, COLOR_CYAN, COLOR_RESET);
377 | last_skip_message = current_time;
378 | }
379 | }
380 | } else {
381 | static time_t last_debounce_message = 0;
382 | if (difftime(current_time, last_debounce_message) > 5) {
383 | printf("%s%s[FILE WATCHER] %sDebouncing - %d ms since last notification%s\n",
384 | BOLD, COLOR_BLUE, COLOR_CYAN, (int)ms_since_last, COLOR_RESET);
385 | last_debounce_message = current_time;
386 | }
387 | }
388 | }
389 | usleep(100000);
390 | }
391 |
392 | if (custom_file_wd >= 0) {
393 | inotify_rm_watch(fd, custom_file_wd);
394 | }
395 | inotify_rm_watch(fd, wd);
396 | close(fd);
397 |
398 | free(watcher_args->directory);
399 | free(watcher_args);
400 | free_html_files(html_files);
401 |
402 | return NULL;
403 | }
--------------------------------------------------------------------------------
/src/handle_client.c:
--------------------------------------------------------------------------------
1 | #include "handle_client.h"
2 | #include "websocket.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #define BUFFER_SIZE 1024
13 | #define MAX_PATH_LEN 256
14 | #define READ_TIMEOUT_SECS 5
15 |
16 | static int set_socket_timeout(int sockfd, int seconds) {
17 | struct timeval tv;
18 | tv.tv_sec = seconds;
19 | tv.tv_usec = 0;
20 | if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
21 | perror("setsockopt - SO_RCVTIMEO");
22 | return -1;
23 | }
24 | if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
25 | perror("setsockopt - SO_SNDTIMEO");
26 | return -1;
27 | }
28 |
29 | return 0;
30 | }
31 |
32 | const char* get_content_type(const char* path) {
33 | const char* ext = strrchr(path, '.');
34 | if (ext) {
35 | if (strcmp(ext, ".html") == 0) return "text/html";
36 | if (strcmp(ext, ".css") == 0) return "text/css";
37 | if (strcmp(ext, ".js") == 0) return "application/javascript";
38 | if (strcmp(ext, ".json") == 0) return "application/json";
39 | if (strcmp(ext, ".png") == 0) return "image/png";
40 | if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg";
41 | if (strcmp(ext, ".gif") == 0) return "image/gif";
42 | if (strcmp(ext, ".svg") == 0) return "image/svg+xml";
43 | if (strcmp(ext, ".ico") == 0) return "image/x-icon";
44 | }
45 | return "text/plain";
46 | }
47 |
48 | static int read_request_line(int client_socket, char* buffer, size_t buffer_size) {
49 | if (set_socket_timeout(client_socket, READ_TIMEOUT_SECS) < 0) {
50 | return -1;
51 | }
52 | size_t total_read = 0;
53 | char c;
54 | ssize_t bytes_read;
55 |
56 | while (total_read < buffer_size - 1) {
57 | bytes_read = recv(client_socket, &c, 1, 0);
58 |
59 | if (bytes_read <= 0) {
60 | if (bytes_read < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
61 | fprintf(stderr, "Timeout while reading request\n");
62 | } else if (bytes_read < 0) {
63 | perror("Error reading from socket");
64 | }
65 | return -1;
66 | }
67 | buffer[total_read++] = c;
68 | if (total_read >= 2 &&
69 | buffer[total_read-2] == '\r' &&
70 | buffer[total_read-1] == '\n') {
71 | break;
72 | }
73 | }
74 |
75 | buffer[total_read] = '\0';
76 | return total_read;
77 | }
78 |
79 | void handle_client(int client_socket) {
80 | char buffer[BUFFER_SIZE] = {0};
81 | char path[MAX_PATH_LEN] = {0};
82 | char full_path[MAX_PATH_LEN] = {0};
83 | int file_fd = -1;
84 | if (read_request_line(client_socket, buffer, BUFFER_SIZE) <= 0) {
85 | close(client_socket);
86 | return;
87 | }
88 | if (sscanf(buffer, "GET %s", path) != 1) {
89 | fprintf(stderr, "Invalid request format: %s\n", buffer);
90 | close(client_socket);
91 | return;
92 | }
93 |
94 | while (read_request_line(client_socket, buffer, BUFFER_SIZE) > 0) {
95 | if (strcmp(buffer, "\r\n") == 0) {
96 | break;
97 | }
98 | }
99 | if (strcmp(path, "/") == 0) {
100 | strcpy(path, "/index.html");
101 | }
102 |
103 | snprintf(full_path, MAX_PATH_LEN, "%s%s", HTML_DIR, path);
104 | struct stat file_stat;
105 | if (stat(full_path, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
106 | file_fd = open(full_path, O_RDONLY);
107 | }
108 |
109 | if (file_fd == -1) {
110 | const char* not_found = "HTTP/1.1 404 Not Found\r\nContent-Length: 26\r\nContent-Type: text/html\r\n\r\n404 - Not Found ";
111 | send(client_socket, not_found, strlen(not_found), 0);
112 | } else {
113 | const char* content_type = get_content_type(path);
114 | char headers[BUFFER_SIZE];
115 | snprintf(headers, BUFFER_SIZE,
116 | "HTTP/1.1 200 OK\r\n"
117 | "Content-Length: %ld\r\n"
118 | "Content-Type: %s\r\n"
119 | "Connection: close\r\n"
120 | "\r\n",
121 | file_stat.st_size, content_type);
122 |
123 | if (send(client_socket, headers, strlen(headers), 0) < 0) {
124 | perror("Error sending headers");
125 | close(file_fd);
126 | close(client_socket);
127 | return;
128 | }
129 | ssize_t bytes_read;
130 | while ((bytes_read = read(file_fd, buffer, BUFFER_SIZE)) > 0) {
131 | ssize_t bytes_sent = send(client_socket, buffer, bytes_read, 0);
132 | if (bytes_sent < 0) {
133 | if (errno == EPIPE || errno == ECONNRESET) {
134 | break;
135 | }
136 | perror("Error sending file content");
137 | break;
138 | }
139 | }
140 | close(file_fd);
141 | }
142 | close(client_socket);
143 | }
144 |
145 | int is_websocket_request(const char* request) {
146 | return (strstr(request, "Upgrade: websocket") != NULL) ? 1 : 0;
147 | }
148 |
149 | void handle_websocket_client(int client_socket, ws_clients_t* clients) {
150 | char buffer[BUFFER_SIZE] = {0};
151 | ssize_t bytes_read = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
152 | if (bytes_read <= 0) {
153 | close(client_socket);
154 | return;
155 | }
156 | buffer[bytes_read] = '\0';
157 |
158 | if (process_ws_handshake(client_socket, buffer)) {
159 | add_ws_client(clients, client_socket);
160 | printf("%s%s[WebSocket] %s%sClient connected%s\n",
161 | BOLD, COLOR_BLUE, BOLD, COLOR_GREEN, COLOR_RESET);
162 | } else {
163 | fprintf(stderr, "%s%s[WebSocket] %s%sFailed to process WebSocket handshake%s\n",
164 | BOLD, COLOR_RED, BOLD, COLOR_YELLOW, COLOR_RESET);
165 | close(client_socket);
166 | }
167 | }
168 |
169 | int process_ws_handshake(int client_socket, char* buffer) {
170 | return process_ws_handshake(client_socket, buffer);
171 | }
--------------------------------------------------------------------------------
/src/html_serve.c:
--------------------------------------------------------------------------------
1 | #include "html_serve.h"
2 |
3 | char* serve_html(const char* filename) {
4 | FILE* file = fopen(filename, "r");
5 | if (!file) {
6 | perror("Error opening file");
7 | return NULL;
8 | }
9 | if (fseek(file, 0, SEEK_END) != 0) {
10 | perror("Error seeking to end of file");
11 | fclose(file);
12 | return NULL;
13 | }
14 |
15 | long length = ftell(file);
16 | if (length == -1) {
17 | perror("Error getting file size");
18 | fclose(file);
19 | return NULL;
20 | }
21 |
22 | if (fseek(file, 0, SEEK_SET) != 0) {
23 | perror("Error seeking to beginning of file");
24 | fclose(file);
25 | return NULL;
26 | }
27 |
28 | char* buffer = malloc(length + 1);
29 | if (!buffer) {
30 | perror("Error allocating memory");
31 | fclose(file);
32 | return NULL;
33 | }
34 |
35 | if (fread(buffer, 1, length, file) != length) {
36 | perror("Error reading file");
37 | free(buffer);
38 | fclose(file);
39 | return NULL;
40 | }
41 |
42 | buffer[length] = '\0';
43 | fclose(file);
44 | return buffer;
45 | }
46 |
47 | char* inject_hot_reload_js(char* html_content) {
48 | if (!html_content) {
49 | fprintf(stderr, "Error: Null HTML content passed to inject_hot_reload_js\n");
50 | return NULL;
51 | }
52 |
53 | const char* hot_reload_js =
54 | "\n";
108 |
109 | char* body_close_tag = strstr(html_content, "