Merge pull request 'Rework of web framework' (#266) from Chewing_Bever/vieter:web-rework into dev
	
		
			
	
		
	
	
		
			
				
	
				ci/woodpecker/push/docs Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/lint Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/arch Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/build Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/man Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/test Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/docker Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/deploy Pipeline was successful
				
					Details
				
			
		
	
				
					
				
			
				
	
				ci/woodpecker/push/docs Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/lint Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/arch Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/build Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/man Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/test Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/docker Pipeline was successful
				
					Details
				
			
		
			
				
	
				ci/woodpecker/push/deploy Pipeline was successful
				
					Details
				
			
		
	Reviewed-on: #266pull/270/head
						commit
						3e0a2584fa
					
				|  | @ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||||
|   providing a Git repository |   providing a Git repository | ||||||
| * CLI commands for searching the AUR & directly adding packages | * CLI commands for searching the AUR & directly adding packages | ||||||
| * HTTP routes for removing packages, arch-repos & repos | * HTTP routes for removing packages, arch-repos & repos | ||||||
|  | * All endpoints serving files now support HTTP byte range requests | ||||||
| 
 | 
 | ||||||
| ### Changed | ### Changed | ||||||
| 
 | 
 | ||||||
|  | @ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | ||||||
| * Branch name for 'git' targets is now optional; if not provided, the | * Branch name for 'git' targets is now optional; if not provided, the | ||||||
|   repository will be cloned with the default branch |   repository will be cloned with the default branch | ||||||
| * Build containers now explicitely set the PATH variable | * Build containers now explicitely set the PATH variable | ||||||
|  | * Refactor of web framework | ||||||
| 
 | 
 | ||||||
| ### Removed | ### Removed | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										7
									
								
								Makefile
								
								
								
								
							|  | @ -83,13 +83,6 @@ fmt: | ||||||
| test: | test: | ||||||
| 	$(V) test $(SRC_DIR) | 	$(V) test $(SRC_DIR) | ||||||
| 
 | 
 | ||||||
| # Build & patch the V compiler
 |  | ||||||
| .PHONY: v |  | ||||||
| v: v/v |  | ||||||
| v/v: |  | ||||||
| 	git clone --single-branch https://git.rustybever.be/vieter-v/v v |  | ||||||
| 	make -C v |  | ||||||
| 
 |  | ||||||
| .PHONY: clean | .PHONY: clean | ||||||
| clean: | clean: | ||||||
| 	rm -rf 'data' 'vieter' 'dvieter' 'pvieter' 'vieter.c' 'pkg' 'src/vieter' *.pkg.tar.zst 'suvieter' 'afvieter' '$(SRC_DIR)/_docs' 'docs/public' | 	rm -rf 'data' 'vieter' 'dvieter' 'pvieter' 'vieter.c' 'pkg' 'src/vieter' *.pkg.tar.zst 'suvieter' 'afvieter' '$(SRC_DIR)/_docs' 'docs/public' | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								README.md
								
								
								
								
							
							
						
						
									
										13
									
								
								README.md
								
								
								
								
							|  | @ -37,7 +37,6 @@ that. | ||||||
| 
 | 
 | ||||||
| Besides a V installer, Vieter also requires the following libraries to work: | Besides a V installer, Vieter also requires the following libraries to work: | ||||||
| 
 | 
 | ||||||
| * gc |  | ||||||
| * libarchive | * libarchive | ||||||
| * openssl | * openssl | ||||||
| * sqlite3 | * sqlite3 | ||||||
|  | @ -48,15 +47,9 @@ update`. | ||||||
| 
 | 
 | ||||||
| ### Compiler | ### Compiler | ||||||
| 
 | 
 | ||||||
| Vieter compiles with the standard Vlang compiler. However, I do maintain a | I used to maintain a mirror that tracked the latest master, but nowadays, I | ||||||
| [mirror](https://git.rustybever.be/vieter-v/v). This is to ensure my CI does | maintain a Docker image containing the specific compiler version that Vieter | ||||||
| not break without reason, as I control when & how frequently the mirror is | builds with. Currently, this is V 0.3. | ||||||
| updated to reflect the official repository. |  | ||||||
| 
 |  | ||||||
| If you encounter issues using the latest V compiler, try using my mirror |  | ||||||
| instead. `make v` will clone the repository & build the mirror. Afterwards, |  | ||||||
| prepending any make command with `V_PATH=v/v` tells make to use the locally |  | ||||||
| compiled mirror instead. |  | ||||||
| 
 | 
 | ||||||
| ## Contributing | ## Contributing | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ module client | ||||||
| 
 | 
 | ||||||
| import net.http { Method } | import net.http { Method } | ||||||
| import net.urllib | import net.urllib | ||||||
| import response { Response } | import web.response { Response } | ||||||
| import json | import json | ||||||
| 
 | 
 | ||||||
| pub struct Client { | pub struct Client { | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ module client | ||||||
| 
 | 
 | ||||||
| import models { BuildLog, BuildLogFilter } | import models { BuildLog, BuildLogFilter } | ||||||
| import net.http { Method } | import net.http { Method } | ||||||
| import response { Response } | import web.response { Response } | ||||||
| import time | import time | ||||||
| 
 | 
 | ||||||
| // get_build_logs returns all build logs. | // get_build_logs returns all build logs. | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ module client | ||||||
| 
 | 
 | ||||||
| import models { Target, TargetFilter } | import models { Target, TargetFilter } | ||||||
| import net.http { Method } | import net.http { Method } | ||||||
| import response { Response } | import web.response { Response } | ||||||
| 
 | 
 | ||||||
| // get_targets returns a list of targets, given a filter object. | // get_targets returns a list of targets, given a filter object. | ||||||
| pub fn (c &Client) get_targets(filter TargetFilter) ?[]Target { | pub fn (c &Client) get_targets(filter TargetFilter) ?[]Target { | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ module server | ||||||
| import web | import web | ||||||
| import net.http | import net.http | ||||||
| import net.urllib | import net.urllib | ||||||
| import response { new_data_response, new_response } | import web.response { new_data_response, new_response } | ||||||
| import db | import db | ||||||
| import time | import time | ||||||
| import os | import os | ||||||
|  | @ -12,12 +12,8 @@ import models { BuildLog, BuildLogFilter } | ||||||
| 
 | 
 | ||||||
| // v1_get_logs returns all build logs in the database. A 'target' query param can | // v1_get_logs returns all build logs in the database. A 'target' query param can | ||||||
| // optionally be added to limit the list of build logs to that repository. | // optionally be added to limit the list of build logs to that repository. | ||||||
| ['/api/v1/logs'; get] | ['/api/v1/logs'; auth; get] | ||||||
| fn (mut app App) v1_get_logs() web.Result { | fn (mut app App) v1_get_logs() web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	filter := models.from_params<BuildLogFilter>(app.query) or { | 	filter := models.from_params<BuildLogFilter>(app.query) or { | ||||||
| 		return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) | 		return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) | ||||||
| 	} | 	} | ||||||
|  | @ -27,24 +23,16 @@ fn (mut app App) v1_get_logs() web.Result { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_get_single_log returns the build log with the given id. | // v1_get_single_log returns the build log with the given id. | ||||||
| ['/api/v1/logs/:id'; get] | ['/api/v1/logs/:id'; auth; get] | ||||||
| fn (mut app App) v1_get_single_log(id int) web.Result { | fn (mut app App) v1_get_single_log(id int) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	log := app.db.get_build_log(id) or { return app.not_found() } | 	log := app.db.get_build_log(id) or { return app.not_found() } | ||||||
| 
 | 
 | ||||||
| 	return app.json(http.Status.ok, new_data_response(log)) | 	return app.json(http.Status.ok, new_data_response(log)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_get_log_content returns the actual build log file for the given id. | // v1_get_log_content returns the actual build log file for the given id. | ||||||
| ['/api/v1/logs/:id/content'; get] | ['/api/v1/logs/:id/content'; auth; get] | ||||||
| fn (mut app App) v1_get_log_content(id int) web.Result { | fn (mut app App) v1_get_log_content(id int) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	log := app.db.get_build_log(id) or { return app.not_found() } | 	log := app.db.get_build_log(id) or { return app.not_found() } | ||||||
| 	file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') | 	file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') | ||||||
| 	full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.target_id.str(), log.arch, | 	full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.target_id.str(), log.arch, | ||||||
|  | @ -63,12 +51,8 @@ fn parse_query_time(query string) ?time.Time { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_post_log adds a new log to the database. | // v1_post_log adds a new log to the database. | ||||||
| ['/api/v1/logs'; post] | ['/api/v1/logs'; auth; post] | ||||||
| fn (mut app App) v1_post_log() web.Result { | fn (mut app App) v1_post_log() web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Parse query params | 	// Parse query params | ||||||
| 	start_time_int := app.query['startTime'].int() | 	start_time_int := app.query['startTime'].int() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,17 +2,13 @@ module server | ||||||
| 
 | 
 | ||||||
| import web | import web | ||||||
| import net.http | import net.http | ||||||
| import response { new_data_response, new_response } | import web.response { new_data_response, new_response } | ||||||
| import db | import db | ||||||
| import models { Target, TargetArch, TargetFilter } | import models { Target, TargetArch, TargetFilter } | ||||||
| 
 | 
 | ||||||
| // v1_get_targets returns the current list of targets. | // v1_get_targets returns the current list of targets. | ||||||
| ['/api/v1/targets'; get] | ['/api/v1/targets'; auth; get] | ||||||
| fn (mut app App) v1_get_targets() web.Result { | fn (mut app App) v1_get_targets() web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	filter := models.from_params<TargetFilter>(app.query) or { | 	filter := models.from_params<TargetFilter>(app.query) or { | ||||||
| 		return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) | 		return app.json(http.Status.bad_request, new_response('Invalid query parameters.')) | ||||||
| 	} | 	} | ||||||
|  | @ -22,24 +18,16 @@ fn (mut app App) v1_get_targets() web.Result { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_get_single_target returns the information for a single target. | // v1_get_single_target returns the information for a single target. | ||||||
| ['/api/v1/targets/:id'; get] | ['/api/v1/targets/:id'; auth; get] | ||||||
| fn (mut app App) v1_get_single_target(id int) web.Result { | fn (mut app App) v1_get_single_target(id int) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	repo := app.db.get_target(id) or { return app.not_found() } | 	repo := app.db.get_target(id) or { return app.not_found() } | ||||||
| 
 | 
 | ||||||
| 	return app.json(http.Status.ok, new_data_response(repo)) | 	return app.json(http.Status.ok, new_data_response(repo)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_post_target creates a new target from the provided query string. | // v1_post_target creates a new target from the provided query string. | ||||||
| ['/api/v1/targets'; post] | ['/api/v1/targets'; auth; post] | ||||||
| fn (mut app App) v1_post_target() web.Result { | fn (mut app App) v1_post_target() web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mut params := app.query.clone() | 	mut params := app.query.clone() | ||||||
| 
 | 
 | ||||||
| 	// If a repo is created without specifying the arch, we assume it's meant | 	// If a repo is created without specifying the arch, we assume it's meant | ||||||
|  | @ -63,24 +51,16 @@ fn (mut app App) v1_post_target() web.Result { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_delete_target removes a given target from the server's list. | // v1_delete_target removes a given target from the server's list. | ||||||
| ['/api/v1/targets/:id'; delete] | ['/api/v1/targets/:id'; auth; delete] | ||||||
| fn (mut app App) v1_delete_target(id int) web.Result { | fn (mut app App) v1_delete_target(id int) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	app.db.delete_target(id) | 	app.db.delete_target(id) | ||||||
| 
 | 
 | ||||||
| 	return app.json(http.Status.ok, new_response('Repo removed successfully.')) | 	return app.json(http.Status.ok, new_response('Repo removed successfully.')) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // v1_patch_target updates a target's data with the given query params. | // v1_patch_target updates a target's data with the given query params. | ||||||
| ['/api/v1/targets/:id'; patch] | ['/api/v1/targets/:id'; auth; patch] | ||||||
| fn (mut app App) v1_patch_target(id int) web.Result { | fn (mut app App) v1_patch_target(id int) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	app.db.update_target(id, app.query) | 	app.db.update_target(id, app.query) | ||||||
| 
 | 
 | ||||||
| 	if 'arch' in app.query { | 	if 'arch' in app.query { | ||||||
|  |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| module server |  | ||||||
| 
 |  | ||||||
| import net.http |  | ||||||
| 
 |  | ||||||
| // is_authorized checks whether the provided API key is correct. |  | ||||||
| fn (mut app App) is_authorized() bool { |  | ||||||
| 	x_header := app.req.header.get_custom('X-Api-Key', http.HeaderQueryConfig{ exact: true }) or { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return x_header.trim_space() == app.conf.api_key |  | ||||||
| } |  | ||||||
|  | @ -7,13 +7,13 @@ import time | ||||||
| import rand | import rand | ||||||
| import util | import util | ||||||
| import net.http | import net.http | ||||||
| import response { new_response } | import web.response { new_response } | ||||||
| 
 | 
 | ||||||
| // healthcheck just returns a string, but can be used to quickly check if the | // healthcheck just returns a string, but can be used to quickly check if the | ||||||
| // server is still responsive. | // server is still responsive. | ||||||
| ['/health'; get] | ['/health'; get] | ||||||
| pub fn (mut app App) healthcheck() web.Result { | pub fn (mut app App) healthcheck() web.Result { | ||||||
| 	return app.json(http.Status.ok, new_response('Healthy.')) | 	return app.json(.ok, new_response('Healthy.')) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // get_repo_file handles all Pacman-related routes. It returns both the | // get_repo_file handles all Pacman-related routes. It returns both the | ||||||
|  | @ -45,25 +45,12 @@ fn (mut app App) get_repo_file(repo string, arch string, filename string) web.Re | ||||||
| 		full_path = os.join_path(app.repo.repos_dir, repo, arch, filename, 'desc') | 		full_path = os.join_path(app.repo.repos_dir, repo, arch, filename, 'desc') | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Scuffed way to respond to HEAD requests |  | ||||||
| 	if app.req.method == http.Method.head { |  | ||||||
| 		if os.exists(full_path) { |  | ||||||
| 			return app.status(http.Status.ok) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return app.not_found() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return app.file(full_path) | 	return app.file(full_path) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // put_package handles publishing a package to a repository. | // put_package handles publishing a package to a repository. | ||||||
| ['/:repo/publish'; post] | ['/:repo/publish'; auth; post] | ||||||
| fn (mut app App) put_package(repo string) web.Result { | fn (mut app App) put_package(repo string) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	mut pkg_path := '' | 	mut pkg_path := '' | ||||||
| 
 | 
 | ||||||
| 	if length := app.req.header.get(.content_length) { | 	if length := app.req.header.get(.content_length) { | ||||||
|  |  | ||||||
|  | @ -2,15 +2,11 @@ module server | ||||||
| 
 | 
 | ||||||
| import web | import web | ||||||
| import net.http | import net.http | ||||||
| import response { new_response } | import web.response { new_response } | ||||||
| 
 | 
 | ||||||
| // delete_package tries to remove the given package. | // delete_package tries to remove the given package. | ||||||
| ['/:repo/:arch/:pkg'; delete] | ['/:repo/:arch/:pkg'; auth; delete] | ||||||
| fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { | fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { | 	res := app.repo.remove_pkg_from_arch_repo(repo, arch, pkg, true) or { | ||||||
| 		app.lerror('Error while deleting package: $err.msg()') | 		app.lerror('Error while deleting package: $err.msg()') | ||||||
| 
 | 
 | ||||||
|  | @ -29,12 +25,8 @@ fn (mut app App) delete_package(repo string, arch string, pkg string) web.Result | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // delete_arch_repo tries to remove the given arch-repo. | // delete_arch_repo tries to remove the given arch-repo. | ||||||
| ['/:repo/:arch'; delete] | ['/:repo/:arch'; auth; delete] | ||||||
| fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { | fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	res := app.repo.remove_arch_repo(repo, arch) or { | 	res := app.repo.remove_arch_repo(repo, arch) or { | ||||||
| 		app.lerror('Error while deleting arch-repo: $err.msg()') | 		app.lerror('Error while deleting arch-repo: $err.msg()') | ||||||
| 
 | 
 | ||||||
|  | @ -53,12 +45,8 @@ fn (mut app App) delete_arch_repo(repo string, arch string) web.Result { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // delete_repo tries to remove the given repo. | // delete_repo tries to remove the given repo. | ||||||
| ['/:repo'; delete] | ['/:repo'; auth; delete] | ||||||
| fn (mut app App) delete_repo(repo string) web.Result { | fn (mut app App) delete_repo(repo string) web.Result { | ||||||
| 	if !app.is_authorized() { |  | ||||||
| 		return app.json(http.Status.unauthorized, new_response('Unauthorized.')) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	res := app.repo.remove_repo(repo) or { | 	res := app.repo.remove_repo(repo) or { | ||||||
| 		app.lerror('Error while deleting repo: $err.msg()') | 		app.lerror('Error while deleting repo: $err.msg()') | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -73,6 +73,7 @@ pub fn server(conf Config) ? { | ||||||
| 
 | 
 | ||||||
| 	web.run(&App{ | 	web.run(&App{ | ||||||
| 		logger: logger | 		logger: logger | ||||||
|  | 		api_key: conf.api_key | ||||||
| 		conf: conf | 		conf: conf | ||||||
| 		repo: repo | 		repo: repo | ||||||
| 		db: db | 		db: db | ||||||
|  |  | ||||||
|  | @ -0,0 +1,133 @@ | ||||||
|  | module web | ||||||
|  | 
 | ||||||
|  | import net.http | ||||||
|  | 
 | ||||||
|  | // A dummy structure that returns from routes to indicate that you actually sent something to a user | ||||||
|  | [noinit] | ||||||
|  | pub struct Result {} | ||||||
|  | 
 | ||||||
|  | pub const ( | ||||||
|  | 	methods_with_form = [http.Method.post, .put, .patch] | ||||||
|  | 	headers_close     = http.new_custom_header_from_map({ | ||||||
|  | 		'Server':                           'Vieter' | ||||||
|  | 		http.CommonHeader.connection.str(): 'close' | ||||||
|  | 	}) or { panic('should never fail') } | ||||||
|  | 
 | ||||||
|  | 	http_302          = http.new_response( | ||||||
|  | 		status: .found | ||||||
|  | 		body: '302 Found' | ||||||
|  | 		header: headers_close | ||||||
|  | 	) | ||||||
|  | 	http_400          = http.new_response( | ||||||
|  | 		status: .bad_request | ||||||
|  | 		body: '400 Bad Request' | ||||||
|  | 		header: http.new_header( | ||||||
|  | 			key: .content_type | ||||||
|  | 			value: 'text/plain' | ||||||
|  | 		).join(headers_close) | ||||||
|  | 	) | ||||||
|  | 	http_401          = http.new_response( | ||||||
|  | 		status: .unauthorized | ||||||
|  | 		body: '401 Unauthorized' | ||||||
|  | 		header: http.new_header( | ||||||
|  | 			key: .content_type | ||||||
|  | 			value: 'text/plain' | ||||||
|  | 		).join(headers_close) | ||||||
|  | 	) | ||||||
|  | 	http_404          = http.new_response( | ||||||
|  | 		status: .not_found | ||||||
|  | 		body: '404 Not Found' | ||||||
|  | 		header: http.new_header( | ||||||
|  | 			key: .content_type | ||||||
|  | 			value: 'text/plain' | ||||||
|  | 		).join(headers_close) | ||||||
|  | 	) | ||||||
|  | 	http_500          = http.new_response( | ||||||
|  | 		status: .internal_server_error | ||||||
|  | 		body: '500 Internal Server Error' | ||||||
|  | 		header: http.new_header( | ||||||
|  | 			key: .content_type | ||||||
|  | 			value: 'text/plain' | ||||||
|  | 		).join(headers_close) | ||||||
|  | 	) | ||||||
|  | 	mime_types        = { | ||||||
|  | 		'.aac':    'audio/aac' | ||||||
|  | 		'.abw':    'application/x-abiword' | ||||||
|  | 		'.arc':    'application/x-freearc' | ||||||
|  | 		'.avi':    'video/x-msvideo' | ||||||
|  | 		'.azw':    'application/vnd.amazon.ebook' | ||||||
|  | 		'.bin':    'application/octet-stream' | ||||||
|  | 		'.bmp':    'image/bmp' | ||||||
|  | 		'.bz':     'application/x-bzip' | ||||||
|  | 		'.bz2':    'application/x-bzip2' | ||||||
|  | 		'.cda':    'application/x-cdf' | ||||||
|  | 		'.csh':    'application/x-csh' | ||||||
|  | 		'.css':    'text/css' | ||||||
|  | 		'.csv':    'text/csv' | ||||||
|  | 		'.doc':    'application/msword' | ||||||
|  | 		'.docx':   'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | ||||||
|  | 		'.eot':    'application/vnd.ms-fontobject' | ||||||
|  | 		'.epub':   'application/epub+zip' | ||||||
|  | 		'.gz':     'application/gzip' | ||||||
|  | 		'.gif':    'image/gif' | ||||||
|  | 		'.htm':    'text/html' | ||||||
|  | 		'.html':   'text/html' | ||||||
|  | 		'.ico':    'image/vnd.microsoft.icon' | ||||||
|  | 		'.ics':    'text/calendar' | ||||||
|  | 		'.jar':    'application/java-archive' | ||||||
|  | 		'.jpeg':   'image/jpeg' | ||||||
|  | 		'.jpg':    'image/jpeg' | ||||||
|  | 		'.js':     'text/javascript' | ||||||
|  | 		'.json':   'application/json' | ||||||
|  | 		'.jsonld': 'application/ld+json' | ||||||
|  | 		'.mid':    'audio/midi audio/x-midi' | ||||||
|  | 		'.midi':   'audio/midi audio/x-midi' | ||||||
|  | 		'.mjs':    'text/javascript' | ||||||
|  | 		'.mp3':    'audio/mpeg' | ||||||
|  | 		'.mp4':    'video/mp4' | ||||||
|  | 		'.mpeg':   'video/mpeg' | ||||||
|  | 		'.mpkg':   'application/vnd.apple.installer+xml' | ||||||
|  | 		'.odp':    'application/vnd.oasis.opendocument.presentation' | ||||||
|  | 		'.ods':    'application/vnd.oasis.opendocument.spreadsheet' | ||||||
|  | 		'.odt':    'application/vnd.oasis.opendocument.text' | ||||||
|  | 		'.oga':    'audio/ogg' | ||||||
|  | 		'.ogv':    'video/ogg' | ||||||
|  | 		'.ogx':    'application/ogg' | ||||||
|  | 		'.opus':   'audio/opus' | ||||||
|  | 		'.otf':    'font/otf' | ||||||
|  | 		'.png':    'image/png' | ||||||
|  | 		'.pdf':    'application/pdf' | ||||||
|  | 		'.php':    'application/x-httpd-php' | ||||||
|  | 		'.ppt':    'application/vnd.ms-powerpoint' | ||||||
|  | 		'.pptx':   'application/vnd.openxmlformats-officedocument.presentationml.presentation' | ||||||
|  | 		'.rar':    'application/vnd.rar' | ||||||
|  | 		'.rtf':    'application/rtf' | ||||||
|  | 		'.sh':     'application/x-sh' | ||||||
|  | 		'.svg':    'image/svg+xml' | ||||||
|  | 		'.swf':    'application/x-shockwave-flash' | ||||||
|  | 		'.tar':    'application/x-tar' | ||||||
|  | 		'.tif':    'image/tiff' | ||||||
|  | 		'.tiff':   'image/tiff' | ||||||
|  | 		'.ts':     'video/mp2t' | ||||||
|  | 		'.ttf':    'font/ttf' | ||||||
|  | 		'.txt':    'text/plain' | ||||||
|  | 		'.vsd':    'application/vnd.visio' | ||||||
|  | 		'.wav':    'audio/wav' | ||||||
|  | 		'.weba':   'audio/webm' | ||||||
|  | 		'.webm':   'video/webm' | ||||||
|  | 		'.webp':   'image/webp' | ||||||
|  | 		'.woff':   'font/woff' | ||||||
|  | 		'.woff2':  'font/woff2' | ||||||
|  | 		'.xhtml':  'application/xhtml+xml' | ||||||
|  | 		'.xls':    'application/vnd.ms-excel' | ||||||
|  | 		'.xlsx':   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | ||||||
|  | 		'.xml':    'application/xml' | ||||||
|  | 		'.xul':    'application/vnd.mozilla.xul+xml' | ||||||
|  | 		'.zip':    'application/zip' | ||||||
|  | 		'.3gp':    'video/3gpp' | ||||||
|  | 		'.3g2':    'video/3gpp2' | ||||||
|  | 		'.7z':     'application/x-7z-compressed' | ||||||
|  | 	} | ||||||
|  | 	max_http_post_size = 1024 * 1024 | ||||||
|  | 	default_port       = 8080 | ||||||
|  | ) | ||||||
|  | @ -3,6 +3,10 @@ module web | ||||||
| import net.urllib | import net.urllib | ||||||
| import net.http | import net.http | ||||||
| 
 | 
 | ||||||
|  | // Method attributes that should be ignored when parsing, as they're used | ||||||
|  | // elsewhere. | ||||||
|  | const attrs_to_ignore = ['auth'] | ||||||
|  | 
 | ||||||
| // Parsing function attributes for methods and path. | // Parsing function attributes for methods and path. | ||||||
| fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { | fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { | ||||||
| 	if attrs.len == 0 { | 	if attrs.len == 0 { | ||||||
|  | @ -32,7 +36,7 @@ fn parse_attrs(name string, attrs []string) ?([]http.Method, string) { | ||||||
| 		} | 		} | ||||||
| 		i++ | 		i++ | ||||||
| 	} | 	} | ||||||
| 	if x.len > 0 { | 	if x.len > 0 && x.any(!web.attrs_to_ignore.contains(it)) { | ||||||
| 		return IError(http.UnexpectedExtraAttributeError{ | 		return IError(http.UnexpectedExtraAttributeError{ | ||||||
| 			attributes: x | 			attributes: x | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
							
								
								
									
										448
									
								
								src/web/web.v
								
								
								
								
							
							
						
						
									
										448
									
								
								src/web/web.v
								
								
								
								
							|  | @ -12,146 +12,25 @@ import time | ||||||
| import json | import json | ||||||
| import log | import log | ||||||
| 
 | 
 | ||||||
| // A dummy structure that returns from routes to indicate that you actually sent something to a user |  | ||||||
| [noinit] |  | ||||||
| pub struct Result {} |  | ||||||
| 
 |  | ||||||
| pub const ( |  | ||||||
| 	methods_with_form = [http.Method.post, .put, .patch] |  | ||||||
| 	headers_close     = http.new_custom_header_from_map({ |  | ||||||
| 		'Server':                           'VWeb' |  | ||||||
| 		http.CommonHeader.connection.str(): 'close' |  | ||||||
| 	}) or { panic('should never fail') } |  | ||||||
| 
 |  | ||||||
| 	http_302          = http.new_response( |  | ||||||
| 		status: .found |  | ||||||
| 		body: '302 Found' |  | ||||||
| 		header: headers_close |  | ||||||
| 	) |  | ||||||
| 	http_400          = http.new_response( |  | ||||||
| 		status: .bad_request |  | ||||||
| 		body: '400 Bad Request' |  | ||||||
| 		header: http.new_header( |  | ||||||
| 			key: .content_type |  | ||||||
| 			value: 'text/plain' |  | ||||||
| 		).join(headers_close) |  | ||||||
| 	) |  | ||||||
| 	http_404          = http.new_response( |  | ||||||
| 		status: .not_found |  | ||||||
| 		body: '404 Not Found' |  | ||||||
| 		header: http.new_header( |  | ||||||
| 			key: .content_type |  | ||||||
| 			value: 'text/plain' |  | ||||||
| 		).join(headers_close) |  | ||||||
| 	) |  | ||||||
| 	http_500          = http.new_response( |  | ||||||
| 		status: .internal_server_error |  | ||||||
| 		body: '500 Internal Server Error' |  | ||||||
| 		header: http.new_header( |  | ||||||
| 			key: .content_type |  | ||||||
| 			value: 'text/plain' |  | ||||||
| 		).join(headers_close) |  | ||||||
| 	) |  | ||||||
| 	mime_types        = { |  | ||||||
| 		'.aac':    'audio/aac' |  | ||||||
| 		'.abw':    'application/x-abiword' |  | ||||||
| 		'.arc':    'application/x-freearc' |  | ||||||
| 		'.avi':    'video/x-msvideo' |  | ||||||
| 		'.azw':    'application/vnd.amazon.ebook' |  | ||||||
| 		'.bin':    'application/octet-stream' |  | ||||||
| 		'.bmp':    'image/bmp' |  | ||||||
| 		'.bz':     'application/x-bzip' |  | ||||||
| 		'.bz2':    'application/x-bzip2' |  | ||||||
| 		'.cda':    'application/x-cdf' |  | ||||||
| 		'.csh':    'application/x-csh' |  | ||||||
| 		'.css':    'text/css' |  | ||||||
| 		'.csv':    'text/csv' |  | ||||||
| 		'.doc':    'application/msword' |  | ||||||
| 		'.docx':   'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |  | ||||||
| 		'.eot':    'application/vnd.ms-fontobject' |  | ||||||
| 		'.epub':   'application/epub+zip' |  | ||||||
| 		'.gz':     'application/gzip' |  | ||||||
| 		'.gif':    'image/gif' |  | ||||||
| 		'.htm':    'text/html' |  | ||||||
| 		'.html':   'text/html' |  | ||||||
| 		'.ico':    'image/vnd.microsoft.icon' |  | ||||||
| 		'.ics':    'text/calendar' |  | ||||||
| 		'.jar':    'application/java-archive' |  | ||||||
| 		'.jpeg':   'image/jpeg' |  | ||||||
| 		'.jpg':    'image/jpeg' |  | ||||||
| 		'.js':     'text/javascript' |  | ||||||
| 		'.json':   'application/json' |  | ||||||
| 		'.jsonld': 'application/ld+json' |  | ||||||
| 		'.mid':    'audio/midi audio/x-midi' |  | ||||||
| 		'.midi':   'audio/midi audio/x-midi' |  | ||||||
| 		'.mjs':    'text/javascript' |  | ||||||
| 		'.mp3':    'audio/mpeg' |  | ||||||
| 		'.mp4':    'video/mp4' |  | ||||||
| 		'.mpeg':   'video/mpeg' |  | ||||||
| 		'.mpkg':   'application/vnd.apple.installer+xml' |  | ||||||
| 		'.odp':    'application/vnd.oasis.opendocument.presentation' |  | ||||||
| 		'.ods':    'application/vnd.oasis.opendocument.spreadsheet' |  | ||||||
| 		'.odt':    'application/vnd.oasis.opendocument.text' |  | ||||||
| 		'.oga':    'audio/ogg' |  | ||||||
| 		'.ogv':    'video/ogg' |  | ||||||
| 		'.ogx':    'application/ogg' |  | ||||||
| 		'.opus':   'audio/opus' |  | ||||||
| 		'.otf':    'font/otf' |  | ||||||
| 		'.png':    'image/png' |  | ||||||
| 		'.pdf':    'application/pdf' |  | ||||||
| 		'.php':    'application/x-httpd-php' |  | ||||||
| 		'.ppt':    'application/vnd.ms-powerpoint' |  | ||||||
| 		'.pptx':   'application/vnd.openxmlformats-officedocument.presentationml.presentation' |  | ||||||
| 		'.rar':    'application/vnd.rar' |  | ||||||
| 		'.rtf':    'application/rtf' |  | ||||||
| 		'.sh':     'application/x-sh' |  | ||||||
| 		'.svg':    'image/svg+xml' |  | ||||||
| 		'.swf':    'application/x-shockwave-flash' |  | ||||||
| 		'.tar':    'application/x-tar' |  | ||||||
| 		'.tif':    'image/tiff' |  | ||||||
| 		'.tiff':   'image/tiff' |  | ||||||
| 		'.ts':     'video/mp2t' |  | ||||||
| 		'.ttf':    'font/ttf' |  | ||||||
| 		'.txt':    'text/plain' |  | ||||||
| 		'.vsd':    'application/vnd.visio' |  | ||||||
| 		'.wav':    'audio/wav' |  | ||||||
| 		'.weba':   'audio/webm' |  | ||||||
| 		'.webm':   'video/webm' |  | ||||||
| 		'.webp':   'image/webp' |  | ||||||
| 		'.woff':   'font/woff' |  | ||||||
| 		'.woff2':  'font/woff2' |  | ||||||
| 		'.xhtml':  'application/xhtml+xml' |  | ||||||
| 		'.xls':    'application/vnd.ms-excel' |  | ||||||
| 		'.xlsx':   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' |  | ||||||
| 		'.xml':    'application/xml' |  | ||||||
| 		'.xul':    'application/vnd.mozilla.xul+xml' |  | ||||||
| 		'.zip':    'application/zip' |  | ||||||
| 		'.3gp':    'video/3gpp' |  | ||||||
| 		'.3g2':    'video/3gpp2' |  | ||||||
| 		'.7z':     'application/x-7z-compressed' |  | ||||||
| 	} |  | ||||||
| 	max_http_post_size = 1024 * 1024 |  | ||||||
| 	default_port       = 8080 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // The Context struct represents the Context which hold the HTTP request and response. | // The Context struct represents the Context which hold the HTTP request and response. | ||||||
| // It has fields for the query, form, files. | // It has fields for the query, form, files. | ||||||
| pub struct Context { | pub struct Context { | ||||||
| mut: |  | ||||||
| 	content_type string      = 'text/plain' |  | ||||||
| 	status       http.Status = http.Status.ok |  | ||||||
| pub: | pub: | ||||||
| 	// HTTP Request | 	// HTTP Request | ||||||
| 	req http.Request | 	req http.Request | ||||||
|  | 	// API key used when authenticating requests | ||||||
|  | 	api_key string | ||||||
| 	// TODO Response | 	// TODO Response | ||||||
| pub mut: | pub mut: | ||||||
| 	done bool |  | ||||||
| 	// time.ticks() from start of web connection handle. |  | ||||||
| 	// You can use it to determine how much time is spent on your request. |  | ||||||
| 	page_gen_start i64 |  | ||||||
| 	// TCP connection to client. | 	// TCP connection to client. | ||||||
| 	// But beware, do not store it for further use, after request processing web will close connection. | 	// But beware, do not store it for further use, after request processing web will close connection. | ||||||
| 	conn &net.TcpConn | 	conn &net.TcpConn | ||||||
|  | 	// Gives access to a shared logger object | ||||||
|  | 	logger shared log.Log | ||||||
|  | 	// time.ticks() from start of web connection handle. | ||||||
|  | 	// You can use it to determine how much time is spent on your request. | ||||||
|  | 	page_gen_start i64 | ||||||
|  | 	// REQUEST | ||||||
| 	static_files      map[string]string | 	static_files      map[string]string | ||||||
| 	static_mime_types map[string]string | 	static_mime_types map[string]string | ||||||
| 	// Map containing query params for the route. | 	// Map containing query params for the route. | ||||||
|  | @ -161,14 +40,13 @@ pub mut: | ||||||
| 	form map[string]string | 	form map[string]string | ||||||
| 	// Files from multipart-form. | 	// Files from multipart-form. | ||||||
| 	files map[string][]http.FileData | 	files map[string][]http.FileData | ||||||
| 
 |  | ||||||
| 	header http.Header // response headers |  | ||||||
| 	// ? It doesn't seem to be used anywhere |  | ||||||
| 	form_error string |  | ||||||
| 	// Allows reading the request body | 	// Allows reading the request body | ||||||
| 	reader io.BufferedReader | 	reader io.BufferedReader | ||||||
| 	// Gives access to a shared logger object | 	// RESPONSE | ||||||
| 	logger shared log.Log | 	status       http.Status = http.Status.ok | ||||||
|  | 	content_type string      = 'text/plain' | ||||||
|  | 	// response headers | ||||||
|  | 	header http.Header | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| struct FileData { | struct FileData { | ||||||
|  | @ -188,50 +66,92 @@ struct Route { | ||||||
| // Probably you can use it for check user session cookie or add header. | // Probably you can use it for check user session cookie or add header. | ||||||
| pub fn (ctx Context) before_request() {} | pub fn (ctx Context) before_request() {} | ||||||
| 
 | 
 | ||||||
| // send_string | // send_string writes the given string to the TCP connection socket. | ||||||
| fn send_string(mut conn net.TcpConn, s string) ? { | fn (mut ctx Context) send_string(s string) ? { | ||||||
| 	conn.write(s.bytes())? | 	ctx.conn.write(s.bytes())? | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // send_response_to_client sends a response to the client | // send_reader reads at most `size` bytes from the given reader & writes them | ||||||
| [manualfree] | // to the TCP connection socket. Internally, a 10KB buffer is used, to avoid | ||||||
| pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { | // having to store all bytes in memory at once. | ||||||
| 	if ctx.done { | fn (mut ctx Context) send_reader(mut reader io.Reader, size u64) ? { | ||||||
| 		return false | 	mut buf := []u8{len: 10_000} | ||||||
| 	} | 	mut bytes_left := size | ||||||
| 	ctx.done = true |  | ||||||
| 
 | 
 | ||||||
| 	// build header | 	// Repeat as long as the stream still has data | ||||||
| 	header := http.new_header_from_map({ | 	for bytes_left > 0 { | ||||||
| 		http.CommonHeader.content_type:   mimetype | 		bytes_read := reader.read(mut buf)? | ||||||
| 		http.CommonHeader.content_length: res.len.str() | 		bytes_left -= u64(bytes_read) | ||||||
| 	}).join(ctx.header) |  | ||||||
| 
 | 
 | ||||||
| 	mut resp := http.Response{ | 		mut to_write := bytes_read | ||||||
| 		header: header.join(web.headers_close) | 
 | ||||||
| 		body: res | 		for to_write > 0 { | ||||||
|  | 			bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { break } | ||||||
|  | 
 | ||||||
|  | 			to_write = to_write - bytes_written | ||||||
| 		} | 		} | ||||||
| 	resp.set_version(.v1_1) | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // send_custom_response sends the given http.Response to the client. It can be | ||||||
|  | // used to overwrite the Context object & send a completely custom | ||||||
|  | // http.Response instead. | ||||||
|  | fn (mut ctx Context) send_custom_response(resp &http.Response) ? { | ||||||
|  | 	ctx.send_string(resp.bytestr())? | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // send_response_header constructs a valid HTTP response with an empty body & | ||||||
|  | // sends it to the client. | ||||||
|  | pub fn (mut ctx Context) send_response_header() ? { | ||||||
|  | 	mut resp := http.new_response( | ||||||
|  | 		header: ctx.header.join(headers_close) | ||||||
|  | 	) | ||||||
|  | 	resp.header.add(.content_type, ctx.content_type) | ||||||
| 	resp.set_status(ctx.status) | 	resp.set_status(ctx.status) | ||||||
| 	send_string(mut ctx.conn, resp.bytestr()) or { return false } | 
 | ||||||
|  | 	ctx.send_custom_response(resp)? | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // send is a convenience function for sending the HTTP response with an empty | ||||||
|  | // body. | ||||||
|  | pub fn (mut ctx Context) send() bool { | ||||||
|  | 	return ctx.send_response('') | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // send_response constructs the resulting HTTP response with the given body | ||||||
|  | // string & sends it to the client. | ||||||
|  | pub fn (mut ctx Context) send_response(res string) bool { | ||||||
|  | 	ctx.send_response_header() or { return false } | ||||||
|  | 	ctx.send_string(res) or { return false } | ||||||
|  | 
 | ||||||
| 	return true | 	return true | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // text responds to a request with some plaintext. | // send_reader_response constructs the resulting HTTP response with the given | ||||||
| pub fn (mut ctx Context) text(status http.Status, s string) Result { | // body & streams the reader's contents to the client. | ||||||
| 	ctx.status = status | pub fn (mut ctx Context) send_reader_response(mut reader io.Reader, size u64) bool { | ||||||
|  | 	ctx.send_response_header() or { return false } | ||||||
|  | 	ctx.send_reader(mut reader, size) or { return false } | ||||||
| 
 | 
 | ||||||
| 	ctx.send_response_to_client('text/plain', s) | 	return true | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 	return Result{} | // is_authenticated checks whether the request passes a correct API key. | ||||||
|  | pub fn (ctx &Context) is_authenticated() bool { | ||||||
|  | 	if provided_key := ctx.req.header.get_custom('X-Api-Key') { | ||||||
|  | 		return provided_key == ctx.api_key | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return false | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // json<T> HTTP_OK with json_s as payload with content-type `application/json` | // json<T> HTTP_OK with json_s as payload with content-type `application/json` | ||||||
| pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { | pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { | ||||||
| 	ctx.status = status | 	ctx.status = status | ||||||
|  | 	ctx.content_type = 'application/json' | ||||||
| 
 | 
 | ||||||
| 	json_s := json.encode(j) | 	json_s := json.encode(j) | ||||||
| 	ctx.send_response_to_client('application/json', json_s) | 	ctx.send_response(json_s) | ||||||
| 
 | 
 | ||||||
| 	return Result{} | 	return Result{} | ||||||
| } | } | ||||||
|  | @ -239,119 +159,112 @@ pub fn (mut ctx Context) json<T>(status http.Status, j T) Result { | ||||||
| // file Response HTTP_OK with file as payload | // file Response HTTP_OK with file as payload | ||||||
| // This function manually implements responses because it needs to stream the file contents | // This function manually implements responses because it needs to stream the file contents | ||||||
| pub fn (mut ctx Context) file(f_path string) Result { | pub fn (mut ctx Context) file(f_path string) Result { | ||||||
| 	if ctx.done { | 	// If the file doesn't exist, just respond with a 404 | ||||||
|  | 	if !os.is_file(f_path) { | ||||||
|  | 		ctx.status = .not_found | ||||||
|  | 		ctx.send() | ||||||
|  | 
 | ||||||
| 		return Result{} | 		return Result{} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if !os.is_file(f_path) { | 	ctx.header.add(.accept_ranges, 'bytes') | ||||||
| 		return ctx.not_found() | 
 | ||||||
|  | 	file_size := os.file_size(f_path) | ||||||
|  | 	ctx.header.add(http.CommonHeader.content_length, file_size.str()) | ||||||
|  | 
 | ||||||
|  | 	// A HEAD request only returns the size of the file. | ||||||
|  | 	if ctx.req.method == .head { | ||||||
|  | 		ctx.send() | ||||||
|  | 
 | ||||||
|  | 		return Result{} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// ext := os.file_ext(f_path) | 	mut file := os.open(f_path) or { | ||||||
| 	// data := os.read_file(f_path) or { |  | ||||||
| 	// 	eprint(err.msg()) |  | ||||||
| 	// 	ctx.server_error(500) |  | ||||||
| 	// 	return Result{} |  | ||||||
| 	// } |  | ||||||
| 	// content_type := web.mime_types[ext] |  | ||||||
| 	// if content_type == '' { |  | ||||||
| 	// 	eprintln('no MIME type found for extension $ext') |  | ||||||
| 	// 	ctx.server_error(500) |  | ||||||
| 
 |  | ||||||
| 	// 	return Result{} |  | ||||||
| 	// } |  | ||||||
| 
 |  | ||||||
| 	// First, we return the headers for the request |  | ||||||
| 
 |  | ||||||
| 	// We open the file before sending the headers in case reading fails |  | ||||||
| 	file_size := os.file_size(f_path) |  | ||||||
| 
 |  | ||||||
| 	file := os.open(f_path) or { |  | ||||||
| 		eprintln(err.msg()) | 		eprintln(err.msg()) | ||||||
| 		ctx.server_error(500) | 		ctx.server_error(500) | ||||||
| 		return Result{} | 		return Result{} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// build header | 	defer { | ||||||
| 	header := http.new_header_from_map({ | 		file.close() | ||||||
| 		// http.CommonHeader.content_type:   content_type | 	} | ||||||
| 		http.CommonHeader.content_length: file_size.str() | 
 | ||||||
| 	}).join(ctx.header) | 	// Currently, this only supports a single provided range, e.g. | ||||||
| 
 | 	// bytes=0-1023, and not multiple ranges, e.g. bytes=0-50, 100-150 | ||||||
| 	mut resp := http.Response{ | 	if range_str := ctx.req.header.get(.range) { | ||||||
| 		header: header.join(web.headers_close) | 		mut parts := range_str.split_nth('=', 2) | ||||||
| 	} | 
 | ||||||
| 	resp.set_version(.v1_1) | 		// We only support the 'bytes' range type | ||||||
| 	resp.set_status(ctx.status) | 		if parts[0] != 'bytes' { | ||||||
| 	send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } | 			ctx.status = .requested_range_not_satisfiable | ||||||
| 
 | 			ctx.header.delete(.content_length) | ||||||
| 	mut buf := []u8{len: 1_000_000} | 			ctx.send() | ||||||
| 	mut bytes_left := file_size | 			return Result{} | ||||||
| 
 | 		} | ||||||
| 	// Repeat as long as the stream still has data | 
 | ||||||
| 	for bytes_left > 0 { | 		parts = parts[1].split_nth('-', 2) | ||||||
| 		// TODO check if just breaking here is safe | 
 | ||||||
| 		bytes_read := file.read(mut buf) or { break } | 		start := parts[0].i64() | ||||||
| 		bytes_left -= u64(bytes_read) | 		end := if parts[1] == '' { file_size - 1 } else { parts[1].u64() } | ||||||
| 
 | 
 | ||||||
| 		mut to_write := bytes_read | 		// Either the actual number 0 or the result of an invalid integer | ||||||
| 
 | 		if end == 0 { | ||||||
| 		for to_write > 0 { | 			ctx.status = .requested_range_not_satisfiable | ||||||
| 			// TODO don't just loop infinitely here | 			ctx.header.delete(.content_length) | ||||||
| 			bytes_written := ctx.conn.write(buf[bytes_read - to_write..bytes_read]) or { continue } | 			ctx.send() | ||||||
| 
 | 			return Result{} | ||||||
| 			to_write = to_write - bytes_written | 		} | ||||||
| 		} | 
 | ||||||
|  | 		// Move cursor to start of data to read | ||||||
|  | 		file.seek(start, .start) or { | ||||||
|  | 			ctx.server_error(500) | ||||||
|  | 			return Result{} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		length := end - u64(start) + 1 | ||||||
|  | 
 | ||||||
|  | 		ctx.status = .partial_content | ||||||
|  | 		ctx.header.set(.content_length, length.str()) | ||||||
|  | 		ctx.send_reader_response(mut file, length) | ||||||
|  | 	} else { | ||||||
|  | 		ctx.send_reader_response(mut file, file_size) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.done = true |  | ||||||
| 	return Result{} | 	return Result{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // status responds with an empty textual response, essentially only returning | // status responds with an empty textual response, essentially only returning | ||||||
| // the given status code. | // the given status code. | ||||||
| pub fn (mut ctx Context) status(status http.Status) Result { | pub fn (mut ctx Context) status(status http.Status) Result { | ||||||
| 	return ctx.text(status, '') | 	ctx.status = status | ||||||
|  | 	ctx.send() | ||||||
|  | 
 | ||||||
|  | 	return Result{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // server_error Response a server error | // server_error Response a server error | ||||||
| pub fn (mut ctx Context) server_error(ecode int) Result { | pub fn (mut ctx Context) server_error(ecode int) Result { | ||||||
| 	$if debug { | 	ctx.send_custom_response(http_500) or {} | ||||||
| 		eprintln('> ctx.server_error ecode: $ecode') | 
 | ||||||
| 	} |  | ||||||
| 	if ctx.done { |  | ||||||
| 		return Result{} |  | ||||||
| 	} |  | ||||||
| 	send_string(mut ctx.conn, web.http_500.bytestr()) or {} |  | ||||||
| 	return Result{} | 	return Result{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // redirect Redirect to an url | // redirect Redirect to an url | ||||||
| pub fn (mut ctx Context) redirect(url string) Result { | pub fn (mut ctx Context) redirect(url string) Result { | ||||||
| 	if ctx.done { | 	mut resp := http_302 | ||||||
| 		return Result{} |  | ||||||
| 	} |  | ||||||
| 	ctx.done = true |  | ||||||
| 	mut resp := web.http_302 |  | ||||||
| 	resp.header = resp.header.join(ctx.header) | 	resp.header = resp.header.join(ctx.header) | ||||||
| 	resp.header.add(.location, url) | 	resp.header.add(.location, url) | ||||||
| 	send_string(mut ctx.conn, resp.bytestr()) or { return Result{} } | 
 | ||||||
|  | 	ctx.send_custom_response(resp) or {} | ||||||
|  | 
 | ||||||
| 	return Result{} | 	return Result{} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // not_found Send an not_found response | // not_found Send an not_found response | ||||||
| pub fn (mut ctx Context) not_found() Result { | pub fn (mut ctx Context) not_found() Result { | ||||||
| 	return ctx.status(http.Status.not_found) | 	ctx.send_custom_response(http_404) or {} | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // add_header Adds an header to the response with key and val | 	return Result{} | ||||||
| pub fn (mut ctx Context) add_header(key string, val string) { |  | ||||||
| 	ctx.header.add_custom(key, val) or {} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // get_header Returns the header data from the key |  | ||||||
| pub fn (ctx &Context) get_header(key string) string { |  | ||||||
| 	return ctx.req.header.get_custom(key) or { '' } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface DbInterface { | interface DbInterface { | ||||||
|  | @ -478,6 +391,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | ||||||
| 		static_mime_types: app.static_mime_types | 		static_mime_types: app.static_mime_types | ||||||
| 		reader: reader | 		reader: reader | ||||||
| 		logger: app.logger | 		logger: app.logger | ||||||
|  | 		api_key: app.api_key | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Calling middleware... | 	// Calling middleware... | ||||||
|  | @ -496,31 +410,27 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | ||||||
| 				// Used for route matching | 				// Used for route matching | ||||||
| 				route_words := route.path.split('/').filter(it != '') | 				route_words := route.path.split('/').filter(it != '') | ||||||
| 
 | 
 | ||||||
| 				// Route immediate matches first | 				// Route immediate matches & index files first | ||||||
| 				// For example URL `/register` matches route `/:user`, but `fn register()` | 				// For example URL `/register` matches route `/:user`, but `fn register()` | ||||||
| 				// should be called first. | 				// should be called first. | ||||||
| 				if !route.path.contains('/:') && url_words == route_words { | 				if (!route.path.contains('/:') && url_words == route_words) | ||||||
|  | 					|| (url_words.len == 0 && route_words == ['index'] && method.name == 'index') { | ||||||
|  | 					// Check whether the request is authorised | ||||||
|  | 					if 'auth' in method.attrs && !app.is_authenticated() { | ||||||
|  | 						conn.write(http_401.bytes()) or {} | ||||||
|  | 						return | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
| 					// We found a match | 					// We found a match | ||||||
| 					if head.method == .post && method.args.len > 0 { |  | ||||||
| 						// TODO implement POST requests |  | ||||||
| 						// Populate method args with form values |  | ||||||
| 						// mut args := []string{cap: method.args.len} |  | ||||||
| 						// for param in method.args { |  | ||||||
| 						// 	args << form[param.name] |  | ||||||
| 						// } |  | ||||||
| 						// app.$method(args) |  | ||||||
| 					} else { |  | ||||||
| 					app.$method() | 					app.$method() | ||||||
| 					} | 					return | ||||||
|  | 				} else if params := route_matches(url_words, route_words) { | ||||||
|  | 					// Check whether the request is authorised | ||||||
|  | 					if 'auth' in method.attrs && !app.is_authenticated() { | ||||||
|  | 						conn.write(http_401.bytes()) or {} | ||||||
| 						return | 						return | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 				if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { |  | ||||||
| 					app.$method() |  | ||||||
| 					return |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if params := route_matches(url_words, route_words) { |  | ||||||
| 					method_args := params.clone() | 					method_args := params.clone() | ||||||
| 					if method_args.len != method.args.len { | 					if method_args.len != method.args.len { | ||||||
| 						eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)') | 						eprintln('warning: uneven parameters count ($method.args.len) in `$method.name`, compared to the web route `$method.attrs` ($method_args.len)') | ||||||
|  | @ -532,7 +442,7 @@ fn handle_conn<T>(mut conn net.TcpConn, mut app T, routes map[string]Route) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	// Route not found | 	// Route not found | ||||||
| 	conn.write(web.http_404.bytes()) or {} | 	conn.write(http_404.bytes()) or {} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // route_matches returns wether a route matches | // route_matches returns wether a route matches | ||||||
|  | @ -578,28 +488,6 @@ fn route_matches(url_words []string, route_words []string) ?[]string { | ||||||
| 	return params | 	return params | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ip Returns the ip address from the current user |  | ||||||
| pub fn (ctx &Context) ip() string { |  | ||||||
| 	mut ip := ctx.req.header.get(.x_forwarded_for) or { '' } |  | ||||||
| 	if ip == '' { |  | ||||||
| 		ip = ctx.req.header.get_custom('X-Real-Ip') or { '' } |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if ip.contains(',') { |  | ||||||
| 		ip = ip.all_before(',') |  | ||||||
| 	} |  | ||||||
| 	if ip == '' { |  | ||||||
| 		ip = ctx.conn.peer_ip() or { '' } |  | ||||||
| 	} |  | ||||||
| 	return ip |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // error Set s to the form error |  | ||||||
| pub fn (mut ctx Context) error(s string) { |  | ||||||
| 	println('web error: $s') |  | ||||||
| 	ctx.form_error = s |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // filter Do not delete. | // filter Do not delete. | ||||||
| // It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside web templates | // It used by `vlib/v/gen/c/str_intp.v:130` for string interpolation inside web templates | ||||||
| // TODO: move it to template render | // TODO: move it to template render | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue