@ -0,0 +1,14 @@ | |||
root = true | |||
[*] | |||
indent_style = space | |||
indent_size = 2 | |||
charset = utf-8 | |||
trim_trailing_whitespace = true | |||
insert_final_newline = true | |||
end_of_line = lf | |||
[*.md] | |||
max_line_length = off | |||
trim_trailing_whitespace = false | |||
indent_size = false |
@ -0,0 +1,28 @@ | |||
# ################################################# # | |||
# OPEN WEATHER - https://openweathermap.org/current # | |||
# ################################################# # | |||
OPEN_WEATHER_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | |||
OPEN_WEATHER_CITY_QUERY_NAME=Leeds | |||
OPEN_WEATHER_CITY_DISPLAY_NAME=Leeds | |||
OPEN_WEATHER_UNITS=metric | |||
# ################################################################ # | |||
# UNSPLASH - https://unsplash.com/documentation#get-a-random-photo # | |||
# ################################################################ # | |||
UNSPLASH_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | |||
UNSPLASH_COLLECTION_ID=789734 | |||
# ########################################## # | |||
# STORMGLASS - https://docs.stormglass.io/#/ # | |||
# ########################################## # | |||
STORMGLASS_API_KEY=xxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxx-xxxxxxxxxxxx | |||
STORMGLASS_LATITUDE=46.365891 | |||
STORMGLASS_LONGITUDE=-1.478393 | |||
STORMGLASS_SPOT_NAME=bud-bud | |||
# ###################################################################### # | |||
# BINANCE - https://binance-docs.github.io/apidocs/spot/en/#general-info # | |||
# ###################################################################### # | |||
BINANCE_SYMBOLS="btc ada ltc dot bch" | |||
BINANCE_PAIR=USDT | |||
BINANCE_PAIR_SYMBOL=\$ |
@ -0,0 +1,66 @@ | |||
# Logs | |||
logs | |||
*.log | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
# Runtime data | |||
pids | |||
*.pid | |||
*.seed | |||
*.pid.lock | |||
# Directory for instrumented libs generated by jscoverage/JSCover | |||
lib-cov | |||
# Coverage directory used by tools like istanbul | |||
coverage | |||
# nyc test coverage | |||
.nyc_output | |||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||
.grunt | |||
# Bower dependency directory (https://bower.io/) | |||
bower_components | |||
# node-waf configuration | |||
.lock-wscript | |||
# Compiled binary addons (https://nodejs.org/api/addons.html) | |||
build/Release | |||
# Dependency directories | |||
node_modules/ | |||
jspm_packages/ | |||
# TypeScript v1 declaration files | |||
typings/ | |||
# Optional npm cache directory | |||
.npm | |||
# Optional eslint cache | |||
.eslintcache | |||
# Optional REPL history | |||
.node_repl_history | |||
# Output of 'npm pack' | |||
*.tgz | |||
# Yarn Integrity file | |||
.yarn-integrity | |||
# dotenv environment variables file | |||
.env | |||
# next.js build output | |||
.next | |||
.DS_Store | |||
dist | |||
.cache | |||
development/ |
@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2021 kikiklang | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512.001 512.001" fill="#9ab899"><path d="M435.827 17.45l-2.399-14.879L418.47.723C417.872.648 412.374 0 403.632 0c-28.593 0-83.096 6.534-126.859 50.298-.237.237-.462.477-.696.713V20.077h-40.155v64.508c6.908 3.385 13.626 7.21 20.077 11.507 28.38-18.898 61.71-29.168 95.368-29.168 21.67 0 43.372 4.042 64.503 12.014 5.539 2.088 10.898 4.467 16.103 7.069 8.751-36.467 4.188-66.481 3.854-68.557zM401.699 116.507c-16.587-6.256-33.518-9.428-50.331-9.428-36.411 0-70.032 14.608-95.368 40.806-25.336-26.198-58.958-40.806-95.37-40.806-16.811 0-33.745 3.172-50.33 9.428-52.531 19.818-82.66 67.606-82.66 131.113 0 112.118 60.196 203.499 119.877 244.362 18.782 13.097 41.974 20.02 67.073 20.02 13.859 0 28.031-2.21 41.41-6.426 13.378 4.215 27.55 6.426 41.41 6.426h.007c25.093 0 48.281-6.921 67.061-20.014 57.756-39.539 119.884-132.846 119.884-244.366-.002-63.509-30.13-111.297-82.663-131.115z"/></svg> |
@ -0,0 +1 @@ | |||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2"><path d="M80 419c166.008 10.245 293.533 7.721 318-29-30.423 61.764-76.731 87.911-136 85-95.161 19.886-148.537-6.929-182-56z" fill="#ff837a"/><path d="M398 390l-22-13c-12.307 59.889-165.176 45.087-296 42 138.782 4.742 285.167 12.825 318-29z" fill="#222835"/><path d="M383 357c-1.481 46.681-61.03 63.59-140.147 66.086C181.364 425.026 108.055 418.261 41 410c-24.997-21.429-32.603-40.78-38-73 26.168 33.097 191.17 25.869 380 20z" fill="#e89d4a"/><path d="M383 357c-100.436 4.732-200.099 5.21-294-26-7.544-5.866-11.348-17.538-14-31-23.894.885-46.171 6.035-67 15-5.866 6.213-7.419 14.261-5 24 11.542 6.744 26.853 11.778 45.12 15.47 78.511 15.869 211.634 6.959 334.88 2.53z" fill="#222835"/><path d="M74 289c1.36 17.141 5.568 32.022 15 42.761 130.838 36.218 262.796 39.202 396 5.239l19-68c-41.448 58.916-228.863 42.976-430 20z" fill="#9193b1"/><path d="M504 269c.591-10.699-24.351-20.075-58-29l-12.984 4C417.842 317.792 229.919 294.082 75 289c179.632 15.36 374.816 41.662 429-20z" fill="#222835"/><path d="M5 185C.287 212.376.854 237.615 6 261c31.581 14.826 56.116 26.72 91.343 28.759C256.991 299.002 434.939 306.96 433 243c-201.827 7.469-406.773 16.35-428-58z" fill="#9ab899"/><path d="M5 185c9.537-16.535 45.148-24.743 90-30l5 3c-9.311 13.234-13.565 25.802-15 38 164.029 77.285 396.636 51.352 425 8-96.21 74.454-348.348 56.302-455.587 17.704C23.535 210.59 4.67 197.781 5 185z" fill="#222835"/><path d="M100 158c-8.486 13.092-13.097 25.711-15 38 171.032 64.805 319.893 84.013 423 9.123L489 130c-31.894 72.626-217.505 45.744-389 28z" fill="#ff837a"/><path d="M489 130c-12.29-17.066-41.544-22.476-75-25 9.362 8.33 16.317 16.228 17 23 5.709 35.983-77.456 40.014-141.484 46 90.327 4.851 175.64 5.596 199.484-44z" fill="#222835"/><path d="M414 105c-15.95-28.885-52.291-46.565-100-58-33 2.599-66-3.171-99-15-97.48 3.085-144.513 43.341-172 98 41.567 28.826 111.32 33.949 171.398 38.398C334.527 177.293 456.862 163.129 414 105z" fill="#e89d4a"/><path d="M269.516 3c-12.112 22.185-21.194 45.38-26 70l15-4c7.172-20.947 11.881-43.929 31-55V9c-5.504-1.745-9.741-4.849-13-9l-7 3z" fill="#222835"/></svg> |
@ -0,0 +1,75 @@ | |||
{ | |||
"name": "pomme-page", | |||
"version": "0.2.0", | |||
"description": "Big buttons with easy click startpage for a browser", | |||
"main": "src/index.pug", | |||
"author": "kikiklang", | |||
"license": "MIT", | |||
"scripts": { | |||
"dev": "npm run clean && npx parcel src/index.pug --out-dir development -p 3000", | |||
"build": "npm run clean && npx parcel build src/index.pug --no-cache", | |||
"serve": "npx serve dist", | |||
"clean": "rimraf ./development && rimraf -rf ./.cache && rimraf -rf ./dist", | |||
"prettier": "prettier --write 'src/**/*.pug'", | |||
"stylelint": "stylelint --fix './**/*.css'", | |||
"xo": "xo --env=browser --fix", | |||
"format": "npm run clean && npm run prettier && npm run stylelint && npm run xo" | |||
}, | |||
"devDependencies": { | |||
"@babel/core": "^7.12.9", | |||
"@babel/preset-env": "^7.12.7", | |||
"@prettier/plugin-pug": "^1.13.3", | |||
"autoprefixer": "^9.8.6", | |||
"husky": "^4.3.0", | |||
"parcel-bundler": "^1.12.4", | |||
"prettier": "^2.2.1", | |||
"pug": "^3.0.0", | |||
"rimraf": "^3.0.2", | |||
"stylelint": "^13.7.2", | |||
"stylelint-color-format": "^1.1.0", | |||
"stylelint-config-rational-order": "^0.0.4", | |||
"stylelint-config-standard": "^20.0.0", | |||
"stylelint-group-selectors": "^1.0.8", | |||
"xo": "^0.37.1" | |||
}, | |||
"browserslist": [ | |||
"since 2017-06" | |||
], | |||
"prettier": { | |||
"printWidth": 130, | |||
"singleQuote": true | |||
}, | |||
"postcss": { | |||
"plugins": { | |||
"autoprefixer": { | |||
"grid": true | |||
} | |||
} | |||
}, | |||
"stylelint": { | |||
"plugins": [ | |||
"stylelint-color-format", | |||
"stylelint-group-selectors" | |||
], | |||
"extends": [ | |||
"stylelint-config-standard", | |||
"stylelint-config-rational-order" | |||
], | |||
"rules": { | |||
"selector-type-no-unknown": null, | |||
"plugin/stylelint-group-selectors": true, | |||
"color-format/format": { | |||
"format": "hsl" | |||
} | |||
} | |||
}, | |||
"xo": { | |||
"semicolon": false, | |||
"space": true | |||
}, | |||
"husky": { | |||
"hooks": { | |||
"pre-commit": "npm run format" | |||
} | |||
} | |||
} |
@ -0,0 +1,115 @@ | |||
/************** CRYPTO **************/ | |||
pp-binance { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 2; | |||
grid-row: span 6; | |||
padding: 0 var(--spacing-2); | |||
background-color: var(--module-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
pp-binance-selector { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
height: var(--spacing-4); | |||
} | |||
.binance-selector-button { | |||
padding: 0; | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
background: none; | |||
border: none; | |||
outline: inherit; | |||
cursor: pointer; | |||
} | |||
.binance-selector-button[data-state='active'] { | |||
color: var(--main); | |||
} | |||
.binance-selector-button[data-state='inactive'] { | |||
color: var(--main-light); | |||
} | |||
pp-binance-chart { | |||
display: flex; | |||
flex: 1; | |||
align-items: center; | |||
transform: scaleY(-1); | |||
} | |||
pp-binance-chart-label { | |||
color: var(--main-light); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-0); | |||
text-align: right; | |||
} | |||
pp-binance-footer { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
height: var(--spacing-5); | |||
} | |||
pp-binance-current { | |||
text-align: right; | |||
} | |||
.binance-difference-label, | |||
.binance-current-label { | |||
padding-top: var(--spacing-1); | |||
color: var(--main-light); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-0); | |||
} | |||
.binance-difference-value, | |||
.binance-current-value { | |||
color: var(--main); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-3); | |||
} | |||
.binance-money-unit { | |||
position: relative; | |||
top: -1px; | |||
color: var(--main-light); | |||
font-size: var(--scale-0); | |||
} | |||
pp-binance-loader-container { | |||
display: flex; | |||
grid-column: span 2; | |||
grid-row: span 6; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--pomme-background); | |||
border-radius: 10px; | |||
} | |||
pp-binance-error-container { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 2; | |||
grid-row: span 6; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--search-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
p.binance-error-code { | |||
margin-top: var(--spacing-0); | |||
color: var(--search-background); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
} |
@ -0,0 +1,122 @@ | |||
// //////// CRYPTO //////// // | |||
let cryptoPicked = process.env.BINANCE_SYMBOLS.split(' ')[0].toUpperCase() | |||
const cryptoButtonsContainer = document.querySelector('pp-binance-selector') | |||
const binanceLoaderContainer = document.querySelector('pp-binance-loader-container') | |||
function handleBinanceApiError(historicalDataResponse, currentDataResponse) { | |||
const binanceErrorContainer = document.querySelector('pp-binance-error-container') | |||
const binanceErrorCode = document.querySelector('.binance-error-code') | |||
if (!historicalDataResponse.status <= 200 || historicalDataResponse.status >= 300) { | |||
binanceErrorCode.innerHTML = historicalDataResponse.status | |||
} | |||
if (!currentDataResponse.status <= 200 || currentDataResponse.status >= 300) { | |||
binanceErrorCode.innerHTML = currentDataResponse.status | |||
} | |||
binanceErrorCode.innerHTML = historicalDataResponse.status | |||
binanceLoaderContainer.style.display = 'none' | |||
binanceErrorContainer.style.display = 'flex' | |||
} | |||
function generateButtons() { | |||
const symbolsList = process.env.BINANCE_SYMBOLS.split(' ') | |||
symbolsList.forEach(symbol => { | |||
const newButton = document.createElement('button') | |||
cryptoButtonsContainer.append(newButton) | |||
newButton.className = 'binance-selector-button' | |||
newButton.dataset.state = 'inactive' | |||
newButton.innerHTML = symbol | |||
}) | |||
cryptoButtonsContainer.firstChild.dataset.state = 'active' | |||
} | |||
function generateChartLine(data) { | |||
const svg = document.querySelector('.binance-chart-svg') | |||
const svgContainerWidth = document.querySelector('pp-binance-chart').offsetWidth | |||
const svgContainerHeight = 130 | |||
const maxCryptoValue = Math.max(...data) | |||
const minCryptoValue = Math.min(...data) | |||
const xAxisPixelvalues = index => (svgContainerWidth / (data.length - 1)) * index | |||
const yAxisPixelValues = value => Math.round(((value - minCryptoValue) * svgContainerHeight) / (maxCryptoValue - minCryptoValue)) | |||
const pixelCoordinatesArray = data.map((value, index) => [xAxisPixelvalues(index), yAxisPixelValues(value)]) | |||
const svgPath = pixelCoordinatesArray.map(pixel => pixel[0] === 0 ? `M ${pixel[0]},${pixel[1]}` : `L ${pixel[0]} ${pixel[1]}`).join(' ') | |||
const svgCode = ` | |||
<defs> | |||
<linearGradient id="gradient" x1="0%" x2="100%"> | |||
<stop offset="0%" stop-color="var(--module-background)" /> | |||
<stop offset="20%" stop-color="var(--main)" /> | |||
<stop offset="80%" stop-color="var(--main)" /> | |||
<stop offset="100%" stop-color="var(--module-background)" /> | |||
</linearGradient> | |||
</defs> | |||
<path d="${svgPath}" transform="translate(0, 10)" stroke="url(#gradient)" fill="none" stroke-width="7" stroke-linejoin="round" /> | |||
` | |||
svg.innerHTML = svgCode | |||
} | |||
function completeFooterValues(data) { | |||
const differenceValue = document.querySelector('.binance-difference-value') | |||
const currentValue = document.querySelector('.binance-current-value') | |||
const pairSymbol = process.env.BINANCE_PAIR_SYMBOL | |||
differenceValue.innerHTML = | |||
data.priceChange > 0 ? | |||
`${Math.round(data.priceChange)}<span class="binance-money-unit">${pairSymbol}</span>` : | |||
`${Math.round(data.priceChange)}<span class="binance-money-unit">${pairSymbol}</span>` | |||
currentValue.innerHTML = `${Math.round(data.lastPrice)}<span class="binance-money-unit">${pairSymbol}</span>` | |||
} | |||
function displayBinanceData(historicalData, currentData) { | |||
const filteredHistoricalData = historicalData.map(value => Number(value[1])) | |||
const binanceLoader = document.querySelector('pp-binance-loader-container') | |||
const binanceContainer = document.querySelector('pp-binance') | |||
binanceLoader.style.display = 'none' | |||
binanceContainer.style.display = 'flex' | |||
generateChartLine(filteredHistoricalData) | |||
completeFooterValues(currentData) | |||
} | |||
async function getBinancedata() { | |||
const url = 'https://api.binance.com' | |||
const pair = process.env.BINANCE_PAIR | |||
const klinesPath = '/api/v3/klines' | |||
const klinesParameters = `?symbol=${cryptoPicked}${pair}&interval=1d&limit=7` | |||
const tickerPath = '/api/v3/ticker/24hr' | |||
const tickerParameters = `?symbol=${cryptoPicked}${pair}` | |||
const [historicalDataResponse, currentDataResponse] = await Promise.all([ | |||
fetch(`${url}${klinesPath}${klinesParameters}`), | |||
fetch(`${url}${tickerPath}${tickerParameters}`) | |||
]) | |||
if (historicalDataResponse.ok && currentDataResponse.ok) { | |||
const [historicalDataJsonResponse, currentDataJsonResponse] = await Promise.all([ | |||
historicalDataResponse.json(), | |||
currentDataResponse.json() | |||
]) | |||
return displayBinanceData(historicalDataJsonResponse, currentDataJsonResponse) | |||
} | |||
return handleBinanceApiError(historicalDataResponse, currentDataResponse) | |||
} | |||
function toggleButtons(event) { | |||
const buttonsList = document.querySelectorAll('.binance-selector-button') | |||
buttonsList.forEach(button => { | |||
button.dataset.state = 'inactive' | |||
}) | |||
event.target.dataset.state = 'active' | |||
cryptoPicked = event.target.innerHTML.toUpperCase() | |||
getBinancedata() | |||
} | |||
export {generateButtons, cryptoButtonsContainer, getBinancedata, toggleButtons} |
@ -0,0 +1,25 @@ | |||
// //////// CRYPTO //////// // | |||
pp-binance | |||
pp-binance-selector | |||
pp-binance-chart | |||
svg.binance-chart-svg(version='1.1', xmlns='http://www.w3.org/2000/svg') | |||
pp-binance-chart-label last week | |||
pp-binance-footer | |||
pp-binance-difference | |||
p.binance-difference-label last 24h | |||
p.binance-difference-value | |||
pp-binance-current | |||
p.binance-current-label curr. value | |||
p.binance-current-value | |||
pp-binance-loader-container | |||
pp-loader | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-binance-error-container | |||
p.error-notification API error | |||
p.binance-error-code |
@ -0,0 +1,19 @@ | |||
/************** CLOCK **************/ | |||
pp-clock { | |||
display: flex; | |||
grid-column: span 2; | |||
grid-row: span 2; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--module-background); | |||
border-radius: 10px; | |||
animation: fadein-page 250ms ease-out normal backwards; | |||
animation-delay: 200ms; | |||
} | |||
.pp-time { | |||
color: var(--main); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-3); | |||
} |
@ -0,0 +1,20 @@ | |||
// //////// CLOCK //////// // | |||
function displayClock() { | |||
const days = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'] | |||
const date = new Date() | |||
const h = date.getHours() | |||
const m = date.getMinutes() | |||
const s = date.getSeconds() | |||
const day = days[date.getDay()] | |||
const hour = h < 10 ? `0${h}` : h | |||
const minute = m < 10 ? `0${m}` : m | |||
const second = s < 10 ? `0${s}` : s | |||
document.querySelector('.pp-time').innerHTML = `${day} : ${hour} : ${minute} : ${second}` | |||
setTimeout(displayClock, 1000) | |||
} | |||
export default displayClock |
@ -0,0 +1,4 @@ | |||
// //////// CLOCK //////// // | |||
pp-clock | |||
p.pp-time |
@ -0,0 +1,169 @@ | |||
/******* RESET *******/ | |||
html, | |||
body, | |||
p, | |||
h1, | |||
h2, | |||
h3, | |||
h4, | |||
h5, | |||
h6 { | |||
margin: 0; | |||
padding: 0; | |||
} | |||
button, | |||
input, | |||
select, | |||
textarea { | |||
margin: 0; | |||
} | |||
*, | |||
*::before, | |||
*::after { | |||
box-sizing: border-box; | |||
} | |||
/************** VARIABLES **************/ | |||
:root { | |||
/* colors */ | |||
--main: hsl(220, 21%, 17%); | |||
--main-light: hsl(30, 35%, 40%); | |||
--module-background: hsl(31, 77%, 60%); | |||
--search-background: hsl(4, 100%, 74%); | |||
--pomme-background: hsl(118, 18%, 66%); | |||
--myrtille-background: hsl(235, 17%, 63%); | |||
/* font-family */ | |||
--font-family-sans: system-ui, -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, ubuntu, 'Helvetica Neue', sans-serif; | |||
/* font Modular scale */ | |||
--scale-000: 0.75rem; /* 12px */ | |||
--scale-00: 0.875rem; /* 14px */ | |||
--scale-0: 1rem; /* 16px */ | |||
--scale-1: 1.125rem; /* 18px */ | |||
--scale-2: 1.25rem; /* 20px */ | |||
--scale-3: 1.5rem; /* 24px */ | |||
--scale-4: 1.875rem; /* 30px */ | |||
--scale-5: 2.25rem; /* 36px */ | |||
/* font weights */ | |||
--font-weight-regular: 400; | |||
--font-weight-bold: 700; | |||
/* sizing property -> margin, padding, height */ | |||
--spacing-0000: 4px; | |||
--spacing-000: 8px; | |||
--spacing-00: 12px; | |||
--spacing-0: 16px; | |||
--spacing-1: 20px; | |||
--spacing-2: 32px; | |||
--spacing-3: 48px; | |||
--spacing-4: 64px; | |||
--spacing-5: 96px; | |||
/* Widths applied as max-width */ | |||
--width-00: 120px; | |||
/* Grid gaps */ | |||
--gap-00: 12px; | |||
--gap-0: 16px; | |||
--gap-1: 24px; | |||
} | |||
/************** GLOBAL STYLES **************/ | |||
html { | |||
background-color: var(--main); | |||
} | |||
body { | |||
padding: var(--spacing-4); | |||
font-family: var(--font-family-sans); | |||
} | |||
grid { | |||
display: grid; | |||
grid-auto-flow: dense; | |||
grid-auto-rows: var(--spacing-2); | |||
grid-gap: var(--gap-1); | |||
grid-template-columns: repeat(auto-fit, minmax(var(--width-00), 1fr)); | |||
} | |||
pomme { | |||
grid-row: span 2; | |||
background-color: var(--pomme-background); | |||
border-radius: 10px; | |||
animation: fadein-page 200ms ease-out normal backwards; | |||
} | |||
pp-loader { | |||
display: grid; | |||
grid-gap: var(--gap-00); | |||
grid-template-areas: | |||
'pp-loader-dot pp-loader-dot' | |||
'pp-loader-dot pp-loader-dot'; | |||
grid-template-columns: 1fr 1fr; | |||
grid-template-rows: 1fr 1fr; | |||
} | |||
pp-loader-dot { | |||
width: var(--spacing-1); | |||
height: var(--spacing-1); | |||
background-color: var(--main); | |||
border-radius: 50%; | |||
animation: loader-animation 1s linear 1s infinite alternate; | |||
} | |||
pp-loader-dot:nth-child(1) { | |||
animation-delay: 0s; | |||
} | |||
pp-loader-dot:nth-child(2) { | |||
animation-delay: -0.2s; | |||
} | |||
pp-loader-dot:nth-child(3) { | |||
animation-delay: -0.4s; | |||
} | |||
pp-loader-dot:nth-child(4) { | |||
animation-delay: -0.6s; | |||
} | |||
.error-notification { | |||
color: var(--search-background); | |||
font-weight: var(--font-weight-regular); | |||
font-size: var(--scale-2); | |||
} | |||
/******* ANIMATION *******/ | |||
@keyframes module-display { | |||
0% { opacity: 0; } | |||
100% { opacity: 1; } | |||
} | |||
@keyframes loader-animation { | |||
100% { background-color: var(--module-background); } | |||
} | |||
@keyframes fadein-page { | |||
0% { transform: scale(0); } | |||
70% { transform: scale(1.05); } | |||
100% { transform: scale(1); } | |||
} | |||
/******* IMPORTS *******/ | |||
@import './myrtille/myrtille.css'; | |||
@import './raisin/raisin.css'; | |||
@import './clock/clock.css'; | |||
@import './search/search.css'; | |||
@import './binance/binance.css'; | |||
@import './openweather/openweather.css'; | |||
@import './unsplash/unsplash.css'; | |||
@import './stormglass/stormglass.css'; |
@ -0,0 +1,33 @@ | |||
// //////// INDEX //////// // | |||
import truncateLinkName from './raisin/raisin.js' | |||
import displayClock from './clock/clock.js' | |||
import {buttonsContainer, searchContainer, toggleEngineIcons, sendSearch} from './search/search.js' | |||
import {generateButtons, cryptoButtonsContainer, getBinancedata, toggleButtons} from './binance/binance.js' | |||
import getOpenWeatherData from './openweather/openweather.js' | |||
import getUnsplashData from './unsplash/unsplash.js' | |||
import getStormglassData from './stormglass/stormglass.js' | |||
// Raisin | |||
truncateLinkName() | |||
// Clock | |||
displayClock() | |||
// Search | |||
buttonsContainer.addEventListener('click', toggleEngineIcons) | |||
searchContainer.addEventListener('keypress', sendSearch) | |||
// Binance | |||
generateButtons() | |||
getBinancedata() | |||
cryptoButtonsContainer.addEventListener('click', toggleButtons) | |||
// Openweather | |||
getOpenWeatherData() | |||
// Unsplash | |||
getUnsplashData() | |||
// Stormglass | |||
getStormglassData() |
@ -0,0 +1,56 @@ | |||
include myrtille/myrtille | |||
include raisin/raisin | |||
doctype html | |||
head | |||
meta(charset='UTF-8') | |||
meta(name='viewport', content='width=device-width, initial-scale=1.0') | |||
meta(name='robots', content='noindex') | |||
link(href='./index.css', rel='stylesheet', type='text/css') | |||
link(rel='shortcut icon', href='../assets/icons/logo/logo.svg') | |||
script(type='module', src='./index.js') | |||
title Pomme Page | |||
grid | |||
include clock/clock | |||
pomme | |||
include search/search | |||
pomme | |||
+myrtille('reddit.com/', '0 0 24 24', 'M14.238 15.348a.215.215 0 010 .306c-.465.462-1.194.687-2.231.687l-.008-.002-.008.002c-1.036 0-1.766-.225-2.231-.688a.214.214 0 010-.305.219.219 0 01.307 0c.379.377 1.008.561 1.924.561l.008.002.008-.002c.915 0 1.544-.184 1.924-.561a.219.219 0 01.307 0zm-3.44-2.418a.922.922 0 00-1.845 0c0 .506.414.918.923.918a.92.92 0 00.922-.918zM24 12c0 6.627-5.373 12-12 12S0 18.627 0 12 5.373 0 12 0s12 5.373 12 12zm-5-.129a1.548 1.548 0 00-2.624-1.108c-1.056-.695-2.485-1.137-4.066-1.194l.865-2.724 2.343.549-.003.034c0 .696.569 1.262 1.268 1.262.699 0 1.267-.566 1.267-1.262a1.266 1.266 0 00-2.446-.458l-2.525-.592a.216.216 0 00-.257.145l-.965 3.038c-1.656.02-3.155.466-4.258 1.181A1.546 1.546 0 005 11.871c0 .566.311 1.056.768 1.325-.03.164-.05.331-.05.5 0 2.281 2.805 4.137 6.253 4.137s6.253-1.856 6.253-4.137c0-.16-.017-.317-.044-.472.486-.261.82-.766.82-1.353zm-4.872.141a.921.921 0 00-.922.919.921.921 0 001.844 0 .921.921 0 00-.922-.919z') | |||
+myrtille('github.com/', '0 0 64 64', "M22.8 62.664C9.63 58.7 0 46.4 0 32 0 14.3 14.3 0 32 0s32 14.3 32 32c0 14.3-9.4 26.4-22.3 30.5-.84-.483-1.408-1.2-1.6-2.5-.024-2.5-.179-13.6-1-13.996L39 46c8.666-2.2 15-8.535 15-16 0-3.9-1.1-8.047-3.9-10.966L50 19c1.01-3.3.496-6.2-1-9-3.4-.029-6.3 1.4-8.9 3.942L40 14c-2.4-1.553-13.4-1.4-15.9-.017L24 14c-1.1-2-7-4.6-9-4-2.1 2.4-2.6 5.3-1 8.958L14 19c-3.1 3.1-3.7 7.6-4 11-.4 7.4 8.4 16 15.9 16.001A.157.157 0 0126 46l-.086.001c-.498.1-.961 2.7-.914 3.9-4.8 2.7-9.2-1-11-4-.958-1.6-6-2.6-6-1 .039 1.2 3.1 2.7 4 5 1.4 3.8 9.3 8.8 13 5v5c-.3 1.3-1 2.2-2.1 2.664z") | |||
+myrtille('youtube.com/', '0 0 1024 1024', "M512 0c282.58 0 512 229.42 512 512 0 282.58-229.42 512-512 512C229.42 1024 0 794.58 0 512 0 229.42 229.42 0 512 0zm270 422.988C782 370.563 739.437 328 687.012 328c0 0-116.66-4-175.012-4-58.671 0-177.012 4-177.012 4C282.563 328 240 370.563 240 422.988c-2.846 35.406-3.728 73.675-3.728 106.012 0 29.364 1.527 58.716 3.728 88.012C240 669.437 282.563 712 334.988 712c0 0 118.341 4 177.012 4 58.352 0 175.012-4 175.012-4C739.437 712 782 669.437 782 617.012c0 0 2-72.675 2-105.012 0-29.678-2-89.012-2-89.012zM461 444l147 86-146.012 84L461 444z") | |||
+myrtille('media.yigitcolakoglu.com/', '0 0 512 512', "M256 201.6c-20.4 0-86.2 119.3-76.2 139.4s142.5 19.9 152.4 0-55.7-139.4-76.2-139.4z M256 23.3c-61.6 0-259.8 359.4-229.6 420.1s429.3 60 459.2 0S317.6 23.3 256 23.3zm150.5 367.5c-19.6 39.3-281.1 39.8-300.9 0S215.7 115.5 256 115.5s170.1 235.9 150.5 275.3z") | |||
include binance/binance | |||
include openweather/openweather | |||
+raisin('dev', dev) | |||
include unsplash/unsplash | |||
+raisin('hack', hack) | |||
+myrtille('vault.yigitcolakoglu.com/', '0 0 64 64', "M32 0c17.6 0 32 14.3 32 32S49.6 64 32 64 0 49.6 0 32 14.3 0 32 0zM14 17.539c0-.673.2-1.3.7-1.795A2.5 2.5 0 0116.539 15h30.868A2.59 2.5 0 0150 17.594V39c-2.8 9.1-9.1 14.5-17.4 17.7-.347.135-.73.1-1.078.008C23.4 53.7 17.3 48.2 14 39V17.539zM32 20h13v19c-1.3 5.022-6.3 9.1-13 13V20z") | |||
+myrtille('twitter.com/', '0 0 64 64', "M6 9c-2.5 6.1-2.8 11.8 3 17-2.049.038-3.5-.409-5-1 .1 6.1 3.2 10.2 10 12-2.069.767-4.067.743-6 0 2.5 5.7 6.2 9.3 12 9-5.1 4.1-11.1 5.95-18 6 17.1 9.7 34.4 4.9 43-4 7.8-8.2 14.74-21.2 12-29 .9.246 6.305-5.8 6-6a57.81 57.81 0 01-7 2c2.5-1.9 4.1-4.3 5-7a30.6 30.6 0 01-8 3c-3.5-4-8.6-4.4-14-3-7.5 2-7.9 14.1-7 14 .0 2.8-27.9-6.3-26-13z") | |||
pomme | |||
+myrtille('drive.yigitcolakoglu.com/', '0 0 64 64', "m32.028095 17.446884c-6.630378 0-12.249342 4.49517-13.99122 10.563651-1.517121-3.258998-4.77612-5.506585-8.597016-5.506585-5.169446 0-9.439859 4.270413-9.439859 9.496049 0 5.225637 4.270413 9.49605 9.496049 9.49605 3.764706 0 7.079895-2.247586 8.597015-5.506585 1.685689 6.068481 7.304653 10.563652 13.935031 10.563652 6.574188 0 12.193152-4.438981 13.99122-10.451272 1.517121 3.146619 4.77612 5.338015 8.484636 5.338015 5.225637 0 9.496049-4.270412 9.496049-9.496049s-4.270412-9.43986-9.496049-9.43986c-3.708516 0-6.967515 2.191396-8.484636 5.338016-1.798068-5.956101-7.360843-10.395082-13.99122-10.395082zm0 5.562773c5.000878 0 8.990343 3.989465 8.990343 8.990342 0 5.000879-3.989465 8.990343-8.990343 8.990343s-8.990343-3.989464-8.990343-8.990343c0-5.000875 3.989465-8.99034 8.990343-8.990342zm-22.532046 5.057067c2.191397 0 3.933275 1.74188 3.933275 3.933275 0 2.191396-1.741878 3.933276-3.933275 3.933276-2.191396 0-3.933275-1.74188-3.933275-3.933276 0-2.191395 1.741879-3.933275 3.933275-3.933275zm45.007902 0c2.191396 0 3.933275 1.74188 3.933275 3.933275 0 2.191396-1.741879 3.933276-3.933275 3.933276s-3.933275-1.74188-3.933275-3.933276c.05619-2.191395 1.741879-3.933275 3.933275-3.933275z") | |||
+myrtille('app.daily.dev/', '0 -10 48 48', "M39.8158775,12.9909755 L34.6975003,7.86188232 L37.2553552,2.73457106 L45.5717179,11.0683452 C46.6313388,12.1301846 46.6313388,13.8517664 45.5717179,14.9136059 L35.3358527,25.1709013 C34.2762317,26.2327407 32.5582467,26.2327407 31.4986258,25.1709013 C30.4390048,24.1090619 30.4390048,22.38748 31.4986258,21.3256406 L39.8158775,12.9909755 Z M31.500533,0.800996807 C32.560154,-0.260842615 34.2785836,-0.260397129 35.3382045,0.801442293 L37.2572625,2.72451813 L14.8678078,25.1608484 C13.8081869,26.2226878 12.0897573,26.2222423 11.0301363,25.1604029 L9.11107833,23.237327 L31.500533,0.800996807 Z M21.9047984,7.85272036 L18.0666824,11.698872 L12.9483052,6.56977883 L6.55055609,12.9809226 L11.6689332,18.1100158 L9.11107833,23.237327 L0.794715716,14.9035529 C-0.264905239,13.8417135 -0.264905239,12.1201317 0.794715716,11.0582922 L11.0301363,0.801442293 C12.0897573,-0.260397129 13.8081869,-0.260842615 14.8678078,0.800996807 L21.9047984,7.85272036 Z") | |||
pomme | |||
+myrtille('git.yigitcolakoglu.com/', '0 0 64 64', "m64 17.5c-.1-4.1-1.4-5-2.4-5.1-10.4.6-16.6.8-22 .9v.3.3 11.6l-1.6-.9v-10.7-.3-.3c-5.8 0-10.8-.3-19.9-.8l-1.8-.1c-.5 0-1.2-.1-1.9-.1-.9-.1-2-.1-3.1-.2-3.1 0-6.1 1.3-8.2 3.4-2.2 2.2-3.2 5.4-3.1 9 .2 6 2.8 10.3 7.5 12.8 2.8 1.5 6.4 2.3 10.6 2.5.8 2.8 6.5 11.4 10.9 11.9h19c2.3-.2 6.9-2.2 11.5-13.6 2.8-6.8 4.6-15.3 4.5-20.6zm-47.7 16.8c-5.3-.7-10.1-2.5-11-9.5-.3-2.1.2-4 1.4-5.3s3.1-2 5.6-2h.2c.6 6.9 1.7 11 3.8 16.8zm30.5-4.5c1.4.7 2 2.4 1.3 3.7l-5.3 10.9c-.7 1.4-2.4 2-3.7 1.3l-10.9-5.3c-1.4-.7-2-2.4-1.3-3.7l5.3-10.9c.7-1.4 2.4-2 3.7-1.3l2 1-1.4 2.9c-.2 0-.4 0-.6.1-.9.3-1.4 1.4-1.1 2.3.1.2.2.4.3.5l-2.4 5c-.2 0-.4 0-.6.1-.9.3-1.4 1.4-1.1 2.3s1.4 1.4 2.3 1.1 1.4-1.4 1.1-2.3c-.1-.3-.2-.5-.4-.7l2.4-4.9c.3 0 .5 0 .8-.1l.6-.3c.1 0 .2.1.3.1 1.8.9 2.9 1.4 3.1 2 .3.8-.5 2.4-2 5.4v.1c-.2 0-.4 0-.7.1-.9.3-1.4 1.4-1.1 2.3s1.4 1.4 2.3 1.1 1.4-1.4 1.1-2.3c-.1-.2-.2-.4-.4-.6 0 0 0 0 0-.1 1.8-3.6 2.5-5.2 2.1-6.5-.4-1.1-1.6-1.7-3.8-2.8-.1 0-.2-.1-.3-.1 0-.2 0-.5-.1-.7s-.2-.4-.4-.6l1.1-2.9z") | |||
+myrtille('protonmail.com/', '0 0 64 64', "M8 64V41l21.1 13.2a5.4 5.4 0 005.7 0L56 41v23H8zm0-39C8.3 11.3 18.7 0 32 0c13.43 0 23.9 10.9 24 24.836V35L36.4 48.874A7.8 7.8 0 0127.5 49L8 36V25zm10.006-4C18.218 14.7 24.2 9.6 32 9.68c7.865 0 14 4.8 14 11.32v8H18v-8h.006z") | |||
@ -0,0 +1,23 @@ | |||
/************** MYRTILLE **************/ | |||
.myrtille-link { | |||
display: flex; | |||
grid-row: span 2; | |||
justify-content: center; | |||
align-items: center; | |||
width: 100%; | |||
height: 100%; | |||
background-color: var(--myrtille-background); | |||
border-radius: 10px; | |||
transition: 100ms ease-in-out; | |||
animation: fadein-page 300ms ease-out normal backwards; | |||
} | |||
.myrtille-link:hover { | |||
transform: rotate(5deg); | |||
} | |||
.myrtille-link svg { | |||
width: 50%; | |||
fill: var(--main); | |||
} |
@ -0,0 +1,6 @@ | |||
// //////// MYRTILLE //////// // | |||
mixin myrtille(domain, viewbox, svgPath) | |||
a.myrtille-link(href="https://" + domain, target='_blank', rel='noopener noreferrer') | |||
svg(xmlns='http://www.w3.org/2000/svg', fill-rule='evenodd', viewBox=viewbox) | |||
path(d=svgPath) |
@ -0,0 +1,81 @@ | |||
/************** openweather **************/ | |||
pp-openweather { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 1; | |||
grid-row: span 4; | |||
align-items: center; | |||
padding: var(--spacing-0000) var(--spacing-0); | |||
background-color: var(--module-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
pp-openweather-icons { | |||
display: flex; | |||
flex: 1; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
pp-openweather-icons .pp-openweather-icon[data-state='hide'] { | |||
display: none; | |||
} | |||
pp-openweather-city, | |||
pp-openweather-temp, | |||
pp-openweather-humid { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: baseline; | |||
width: 100%; | |||
height: var(--spacing-2); | |||
} | |||
.city-label, | |||
.temp-label, | |||
.humid-label { | |||
color: var(--main-light); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-0); | |||
} | |||
.city-value, | |||
.temp-value, | |||
.humid-value { | |||
color: var(--main); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-1); | |||
} | |||
pp-openweather-loader-container { | |||
display: flex; | |||
grid-column: span 1; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--pomme-background); | |||
border-radius: 10px; | |||
} | |||
pp-openweather-error-container { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 1; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--search-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
p.openweather-error-code { | |||
margin-top: var(--spacing-0); | |||
color: var(--search-background); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
} |
@ -0,0 +1,51 @@ | |||
// //////// WEATHER //////// // | |||
const openWeatherLoaderContainer = document.querySelector('pp-openweather-loader-container') | |||
function handleopenWeatherApiError(response) { | |||
const openWeatherErrorContainer = document.querySelector('pp-openweather-error-container') | |||
const openWeatherErrorCode = document.querySelector('.openweather-error-code') | |||
openWeatherErrorCode.innerHTML = response.status | |||
openWeatherLoaderContainer.style.display = 'none' | |||
openWeatherErrorContainer.style.display = 'flex' | |||
} | |||
function displayOpenWeatherData(data) { | |||
const openWeatherLoader = document.querySelector('pp-openweather-loader-container') | |||
const openWeatherContainer = document.querySelector('pp-openweather') | |||
const temporary = document.querySelector('.temp-value') | |||
const humid = document.querySelector('.humid-value') | |||
const icons = [...document.querySelectorAll('.pp-openweather-icon')] | |||
const cityName = document.querySelector('.city-value') | |||
icons.forEach(icon => { | |||
if (icon.getAttribute('data-type').includes(data.weather[0].main.toLowerCase())) { | |||
icon.dataset.state = 'show' | |||
} else { | |||
icon.dataset.state = 'hide' | |||
} | |||
}) | |||
cityName.innerHTML = process.env.OPEN_WEATHER_CITY_DISPLAY_NAME || process.env.OPEN_WEATHER_CITY_QUERY_NAME | |||
temporary.innerHTML = data.main.temp > 0 && data.main.temp < 10 ? `0${Math.round(data.main.temp)}°` : `${Math.round(data.main.temp)}°` | |||
humid.innerHTML = `${data.main.humidity}%` | |||
openWeatherLoader.style.display = 'none' | |||
openWeatherContainer.style.display = 'flex' | |||
} | |||
async function getOpenWeatherData() { | |||
const apiKey = process.env.OPEN_WEATHER_API_KEY | |||
const url = 'https://api.openweathermap.org/data/2.5/weather' | |||
const city = process.env.OPEN_WEATHER_CITY_QUERY_NAME | |||
const units = process.env.OPEN_WEATHER_UNITS | |||
const response = await fetch(`${url}?q=${city}&units=${units}&APPID=${apiKey}`) | |||
if (response.ok) { | |||
const jsonResponse = await response.json() | |||
return displayOpenWeatherData(jsonResponse) | |||
} | |||
return handleopenWeatherApiError(response) | |||
} | |||
export default getOpenWeatherData |
@ -0,0 +1,148 @@ | |||
// //////// openweather //////// // | |||
pp-openweather | |||
pp-openweather-icons | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='clear', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
circle(cx='12', cy='12', r='5') | |||
path(d='M12 1v2') | |||
path(d='M12 21v2') | |||
path(d='M4.22 4.22l1.42 1.42') | |||
path(d='M18.36 18.36l1.42 1.42') | |||
path(d='M1 12h2') | |||
path(d='M21 12h2') | |||
path(d='M4.22 19.78l1.42-1.42') | |||
path(d='M18.36 5.64l1.42-1.42') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='clouds', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='drizzle', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M8 19v2') | |||
path(d='M8 13v2') | |||
path(d='M16 19v2') | |||
path(d='M16 13v2') | |||
path(d='M12 21v2') | |||
path(d='M12 15v2') | |||
path(d='M20 16.58A5 5 0 0018 7h-1.26A8 8 0 104 15.25') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='rain', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M16 13v8') | |||
path(d='M8 13v8') | |||
path(d='M12 15v8') | |||
path(d='M20 16.58A5 5 0 0018 7h-1.26A8 8 0 104 15.25') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='snow', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M20 17.58A5 5 0 0018 8h-1.26A8 8 0 104 16.25') | |||
path(d='M8 16h.01') | |||
path(d='M8 20h.01') | |||
path(d='M12 18h.01') | |||
path(d='M12 22h.01') | |||
path(d='M16 16h.01') | |||
path(d='M16 20h.01') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='thunderstorm', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M19 16.9A5 5 0 0018 7h-1.26a8 8 0 10-11.62 9') | |||
path(d='M13 11l-4 6h6l-4 6') | |||
svg.pp-openweather-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
data-state='hide', | |||
data-type='fog', | |||
viewBox='0 0 24 24', | |||
width='64', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='2', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path( | |||
d='M47.07 96.74c1.83 0 3.32 1.65 3.32 3.68 0 2.03-1.48 3.68-3.32 3.68h-9.35c-1.83 0-3.32-1.65-3.32-3.68 0-2.03 1.48-3.68 3.32-3.68h9.35zM14.3 67.94a3.678 3.678 0 01-4.72 5.64c-1.68-1.4-3.15-2.99-4.4-4.72C1.84 64.25.04 58.63 0 53.03c-.04-5.66 1.72-11.29 5.52-15.85 1.23-1.48 2.68-2.84 4.34-4.04 1.93-1.4 4.14-2.58 6.64-3.55 1.72-.67 3.56-1.23 5.5-1.68 2.2-8.74 6.89-15.47 12.92-20.14C40.56 3.4 47.35.85 54.34.18c6.96-.67 14.12.51 20.55 3.6C81.91 7.15 88.03 12.76 92 20.65c1.6-.25 3.2-.38 4.79-.36 6.72.05 13.2 2.45 18.3 7.95 1.07 1.15 2.08 2.45 3.02 3.9 3.2 4.92 4.84 11.49 4.77 17.92-.07 6.31-1.77 12.59-5.25 17.22a3.67 3.67 0 11-5.87-4.41c2.5-3.33 3.73-8.04 3.78-12.87.06-5.07-1.18-10.16-3.59-13.86-.69-1.07-1.45-2.03-2.25-2.89-3.61-3.89-8.19-5.59-12.95-5.62-3.46-.02-7.02.81-10.41 2.31-.75.37-1.5.77-2.25 1.21-2.25 1.32-4.47 2.93-6.74 4.78l-4.84-5.54c1.67-1.55 3.48-2.96 5.4-4.21 1.53-1 3.13-1.88 4.77-2.65.66-.33 1.33-.64 2-.93-3.19-5.65-7.78-9.7-12.98-12.2-5.2-2.49-11.02-3.45-16.69-2.9-5.63.54-11.1 2.59-15.62 6.1-5.23 4.06-9.2 10.11-10.73 18.14l-.48 2.51-2.5.44c-2.45.43-4.64 1.02-6.56 1.77-1.86.72-3.52 1.61-4.97 2.66-1.16.84-2.16 1.78-3.01 2.8-2.63 3.15-3.85 7.1-3.82 11.1a20.07 20.07 0 003.79 11.53 20.07 20.07 0 003.19 3.39zm17.97 4.09c-1.49 0-2.69-1.65-2.69-3.68 0-2.03 1.2-3.68 2.69-3.68h66.56c1.49 0 2.69 1.65 2.69 3.68 0 2.03-1.2 3.68-2.69 3.68H32.27zM20.38 87.54c-1.83 0-3.32-1.65-3.32-3.68s1.48-3.68 3.32-3.68H72.9c1.83 0 3.32 1.65 3.32 3.68s-1.48 3.68-3.32 3.68H20.38zm69.02 0c-1.83 0-3.32-1.65-3.32-3.68s1.48-3.68 3.32-3.68h11.82c1.83 0 3.32 1.65 3.32 3.68s-1.48 3.68-3.32 3.68H89.4zm19.87 9.49c1.82.01 3.3 1.66 3.29 3.68-.01 2.03-1.49 3.66-3.32 3.66l-46.97-.27c-1.82-.01-3.3-1.66-3.29-3.68.01-2.03 1.49-3.66 3.32-3.66l46.97.27z' | |||
) | |||
pp-openweather-city | |||
p.city-label city | |||
p.city-value LRSY | |||
pp-openweather-temp | |||
p.temp-label temp. | |||
p.temp-value | |||
pp-openweather-humid | |||
p.humid-label humid. | |||
p.humid-value | |||
pp-openweather-loader-container | |||
pp-loader | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-openweather-error-container | |||
p.error-notification API error | |||
p.openweather-error-code |
@ -0,0 +1,45 @@ | |||
/************** RAISIN **************/ | |||
raisin { | |||
display: flex; | |||
flex-direction: column; | |||
grid-row: span 4; | |||
justify-content: space-between; | |||
padding: var(--spacing-00) var(--spacing-0); | |||
background-color: var(--main); | |||
border: 2px solid var(--pomme-background); | |||
border-radius: 10px; | |||
animation: fadein-page 50ms ease-out normal backwards; | |||
animation-delay: 50ms; | |||
} | |||
.raisin-title { | |||
color: var(--myrtille-background); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-1); | |||
} | |||
raisin-links { | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
.raisin-link { | |||
padding: var(--spacing-0000) 0; | |||
color: inherit; | |||
color: var(--myrtille-background); | |||
font-size: var(--scale-00); | |||
text-decoration: none; | |||
border-top: 1px dashed var(--myrtille-background); | |||
outline: none; | |||
} | |||
.raisin-link:first-child { | |||
border-top: none; | |||
} | |||
.raisin-link:hover::after { | |||
position: absolute; | |||
content: "❯"; | |||
color: var(--pomme-background); | |||
} |
@ -0,0 +1,14 @@ | |||
// //////// RAISIN //////// // | |||
function truncateLinkName() { | |||
const raisinLinks = [...document.querySelectorAll('.raisin-link')] | |||
const maxLength = 12 | |||
raisinLinks.forEach(link => { | |||
if (link.innerHTML.length > maxLength) { | |||
link.innerHTML = `${link.innerHTML.replace(/\.[^/.]+$/, '').slice(0, Math.max(0, maxLength))}...` | |||
} | |||
}) | |||
} | |||
export default truncateLinkName |
@ -0,0 +1,46 @@ | |||
// //////// RAISIN //////// // | |||
- | |||
var dev = { | |||
'regex101': 'regex101.com', | |||
'codewars': 'codewars.com', | |||
'dev.to': 'dev.to', | |||
'css tricks': 'css-tricks.com' | |||
} | |||
var hack = { | |||
'hackerone': 'hackerone.com', | |||
'bugcrowd': 'bugcrowd.com', | |||
'hackthebox': 'hackthebox.eu', | |||
'intigriti': 'intigriti.com' | |||
} | |||
var colors = { | |||
'coolors': 'coolors.co', | |||
'color space': 'mycolor.space/', | |||
'color hunt': 'colorhunt.co', | |||
'gradient magic': 'gradientmagic.com' | |||
} | |||
var css = { | |||
'fancy borders': '9elements.github.io/fancy-border-radius', | |||
'transform visualizer': 'css-transform.moro.es', | |||
'css doodle': 'css-doodle.com', | |||
'css stats': 'cssstats.com', | |||
} | |||
var fonts = { | |||
'type scale': 'type-scale.com', | |||
'fonts in use': 'fontsinuse.com', | |||
'font joy': 'fontjoy.com', | |||
'zenigata': 'typo.zenigata.fr', | |||
} | |||
mixin raisin(title, data) | |||
raisin | |||
raisin-header | |||
p.raisin-title #{title} | |||
raisin-links | |||
each val, key in data | |||
a.raisin-link(href="https://" + val, target='_blank', rel='noopener noreferrer') #{key} |
@ -0,0 +1,58 @@ | |||
/************** SEARCH **************/ | |||
pp-search { | |||
display: flex; | |||
grid-column: span 3; | |||
grid-row: span 2; | |||
justify-content: space-between; | |||
align-items: center; | |||
padding: 0 var(--spacing-0); | |||
background-color: var(--search-background); | |||
border-radius: 10px; | |||
animation: fadein-page 250ms ease-out normal backwards; | |||
animation-delay: 150ms; | |||
} | |||
pp-engine-buttons { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
width: 80px; | |||
} | |||
.pp-search-icon { | |||
width: var(--spacing-2); | |||
height: var(--spacing-2); | |||
padding: 0; | |||
background: none; | |||
border: none; | |||
outline: inherit; | |||
cursor: pointer; | |||
} | |||
.pp-search-icon[data-state='inactive'] { | |||
opacity: 0.5; | |||
} | |||
.pp-search-icon > path { | |||
pointer-events: none; | |||
} | |||
.pp-search-input { | |||
flex: 1; | |||
min-width: 0; | |||
margin: 0 var(--spacing-000) 0 24px; | |||
padding: var(--spacing-0000) 0; | |||
color: var(--main); | |||
font-size: var(--scale-2); | |||
background-color: transparent; | |||
background-image: none; | |||
border: none; | |||
border-bottom: 1px dashed var(--main); | |||
box-shadow: none; | |||
} | |||
pp-search textarea:focus, | |||
input:focus { | |||
outline: none; | |||
} |
@ -0,0 +1,27 @@ | |||
// //////// SEARCH //////// // | |||
const searchContainer = document.querySelector('pp-search') | |||
const buttonsContainer = document.querySelector('pp-engine-buttons') | |||
const iconList = document.querySelectorAll('.pp-search-icon') | |||
function toggleEngineIcons(event) { | |||
iconList.forEach(icon => { | |||
icon.dataset.state = 'inactive' | |||
}) | |||
event.target.dataset.state = 'active' | |||
} | |||
function sendSearch(event) { | |||
const input = document.querySelector('.pp-search-input') | |||
if (event.key === 'Enter') { | |||
const activeEngine = [...iconList].find(b => [...b.attributes][3].value === 'active') | |||
const activeEngineDomain = [...activeEngine.attributes][5].value | |||
const url = `${activeEngineDomain}?q=${input.value}` | |||
window.open(url) | |||
input.value = '' | |||
} | |||
} | |||
export {buttonsContainer, searchContainer, toggleEngineIcons, sendSearch} |
@ -0,0 +1,28 @@ | |||
// //////// SEARCH //////// // | |||
pp-search | |||
pp-engine-buttons | |||
svg.pp-search-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
viewBox='0 0 534 534', | |||
data-state='inactive', | |||
data-engine='duckduckgo', | |||
data-address='https://www.duckduckgo.com/', | |||
fill='var(--main)' | |||
) | |||
path( | |||
d='M175.556 517.337C73.183 480.071 0 381.848 0 266.667 0 119.489 119.489 0 266.667 0c147.177 0 266.666 119.489 266.666 266.667 0 125.498-86.88 230.864-203.74 259.185L312.5 491.667c-19.063 4.786-32.54 4.404-41.667 0-15.311 11.875-31.988 20.181-50 25-11.041-16.974-12.847-49.127-8.333-91.667 5.902-5.051 28.026 2.93 62.5 20.833 1.372-4.124 5.432-6.952 12.5-8.333-10.974-31.038-14.996-63.002-12.5-95.833 31.823 19.933 69.278 29.735 112.5 29.166 29.953-2.016 53.515-11.799 70.833-29.166 2.241-5.611-.691-8.301-8.333-8.334-17.001-1.374-45.382 5.703-87.5 12.5-18.683-.116-34.105-4.111-45.833-12.5-14.395-9.696-13.463-15.31-4.167-25 5.345-5.359 29.53-4.833 54.167-4.166 28.74-2.514 102.777-34.723 116.666-45.834 10.682-8.545-15.23-22.754-33.333-20.833l-83.333 25C371.312 167.894 336.725 118.358 275 100c-33.955-40.195-83.302-50.067-141.667-41.667l25 8.334-29.166 25c25.938-2.216 53.635-.986 83.333 4.166-26.637 5.01-47.69 13.217-62.5 25-24.723 17.544-37.223 43.933-37.5 79.167 16.804 103.667 40.37 216.46 63.056 317.337zm16.111-313.17c11.498 0 20.833 9.335 20.833 20.833 0 11.498-9.335 20.833-20.833 20.833-11.499 0-20.834-9.335-20.834-20.833 0-11.498 9.335-20.833 20.834-20.833zm139.583-8.334c10.348 0 18.75 8.402 18.75 18.75 0 10.349-8.402 18.75-18.75 18.75s-18.75-8.401-18.75-18.75c0-10.348 8.402-18.75 18.75-18.75zm-133.333 20.834c3.449 0 6.25 2.8 6.25 6.25a6.254 6.254 0 01-6.25 6.25 6.253 6.253 0 01-6.25-6.25c0-3.45 2.8-6.25 6.25-6.25zm138.958-9.584a5.628 5.628 0 015.625 5.625 5.627 5.627 0 01-5.625 5.625 5.627 5.627 0 01-5.625-5.625 5.628 5.628 0 015.625-5.625zM154.167 187.5c5.383-19.103 19.214-26.131 41.666-20.833-6.781-5.178-22.222-11.806-29.166-8.334-6.945 3.473-15.388 17.721-12.5 29.167zm144.999-20.833c13.116-10.594 27.378-9.636 42.501 0-2.379-6.09-17.917-16.667-25-16.667-7.084 0-16.659 9.732-17.501 16.667z' | |||
) | |||
svg.pp-search-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
viewBox='0 0 200 200', | |||
data-state='active', | |||
data-engine='google', | |||
data-address='https://www.google.com/', | |||
fill-rule='evenodd', | |||
fill='var(--main)' | |||
) | |||
path( | |||
d='M100 0c55.192 0 100 44.808 100 100s-44.808 100-100 100S0 155.192 0 100 44.808 0 100 0zm42.257 115.575h-40.174V87.5h70.834l3.205-.029s.961 8.266.961 12.529c0 43.693-34.54 79.167-77.083 79.167S22.917 143.693 22.917 100c0-43.693 34.54-79.167 77.083-79.167 20.791 0 39.671 8.473 53.542 22.236l-22.547 23.598c-8.06-7.908-18.979-12.768-30.995-12.768-24.774 0-44.888 20.657-44.888 46.101S75.226 146.101 100 146.101c19.453 0 36.033-12.737 42.257-30.526z' | |||
) | |||
input.pp-search-input(type='text', autofocus) |
@ -0,0 +1,91 @@ | |||
/************** STORMGLASS **************/ | |||
pp-stormglass { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
padding: 0 var(--spacing-2); | |||
background-color: var(--module-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
pp-stormglass > * { | |||
display: flex; | |||
justify-content: space-between; | |||
align-items: center; | |||
height: var(--spacing-4); | |||
} | |||
pp-header > p, | |||
.watertemp-value, | |||
.winddir-value, | |||
.windspeed-value, | |||
.waveheight-value, | |||
.waveperiod-value, | |||
.wavedir-value { | |||
color: var(--main); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
} | |||
pp-firstrow > *, | |||
pp-secondrow > * { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
} | |||
.watertemp-label, | |||
.winddir-label, | |||
.windspeed-label, | |||
.waveheight-label, | |||
.waveperiod-label, | |||
.wavedir-label { | |||
color: var(--main-light); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-00); | |||
} | |||
.pp-stormglass-unit { | |||
color: var(--main-light); | |||
font-size: var(--scale-0); | |||
} | |||
pp-linebreak { | |||
width: 100%; | |||
height: 1px; | |||
background: var(--main-light); | |||
} | |||
pp-stormglass-loader-container { | |||
display: flex; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--pomme-background); | |||
border-radius: 10px; | |||
} | |||
pp-stormglass-error-container { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--search-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
p.stormglass-error-code { | |||
margin-top: var(--spacing-0); | |||
color: var(--search-background); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
} |
@ -0,0 +1,53 @@ | |||
// //////// STORMGLASS //////// // | |||
const stormglassLoaderContainer = document.querySelector('pp-stormglass-loader-container') | |||
function handleStormGlassApiError(response) { | |||
const stormglassErrorContainer = document.querySelector('pp-stormglass-error-container') | |||
const stormglassErrorCode = document.querySelector('.stormglass-error-code') | |||
stormglassErrorCode.innerHTML = response.status | |||
stormglassLoaderContainer.style.display = 'none' | |||
stormglassErrorContainer.style.display = 'flex' | |||
} | |||
async function displayStormglassData(data) { | |||
const stormglassContainer = document.querySelector('pp-stormglass') | |||
const spotName = document.querySelector('.header-spot') | |||
const waterTemporary = document.querySelector('.watertemp-value') | |||
const windDirection = document.querySelector('.winddir-icon') | |||
const windSpeed = document.querySelector('.windspeed-value') | |||
const waveHeight = document.querySelector('.waveheight-value') | |||
const wavePeriod = document.querySelector('.waveperiod-value') | |||
const wavedir = document.querySelector('.wavedir-icon') | |||
spotName.innerHTML = process.env.STORMGLASS_SPOT_NAME | |||
waterTemporary.innerHTML = `${Math.round(data.waterTemperature.meto || data.waterTemperature.noaa || data.waterTemperature.sg)}°` | |||
windDirection.style.transform = `rotate(${data.windDirection.icon || data.windDirection.noaa || data.windDirection.sg}deg)` | |||
windSpeed.innerHTML = `${data.windSpeed.icon || data.windSpeed.noaa || data.windSpeed.sg} <span class="pp-stormglass-unit">m/s</span>` | |||
waveHeight.innerHTML = `${data.waveHeight.icon || data.waveHeight.dwd} <span class="pp-stormglass-unit">m</span>` | |||
wavePeriod.innerHTML = `${Math.round(data.wavePeriod.icon || data.wavePeriod.noaa)} <span class="pp-stormglass-unit">s</span>` | |||
wavedir.style.transform = `rotate(${data.waveDirection.icon || data.waveDirection.noaa || data.waveDirection.meteo}deg)` | |||
stormglassLoaderContainer.style.display = 'none' | |||
stormglassContainer.style.display = 'flex' | |||
} | |||
async function getStormglassData() { | |||
const currentISODate = new Date().toISOString().slice(0, 13) | |||
const apiKey = process.env.STORMGLASS_API_KEY | |||
const lat = process.env.STORMGLASS_LATITUDE | |||
const lng = process.env.STORMGLASS_LONGITUDE | |||
const url = 'https://api.stormglass.io/v2/' | |||
const path = 'weather/point' | |||
const parameters = ['waterTemperature', 'waveDirection', 'waveHeight', 'wavePeriod', 'windDirection', 'windSpeed'] | |||
const response = await fetch(`${url}${path}?lat=${lat}&lng=${lng}¶ms=${parameters}`, {headers: {Authorization: apiKey}}) | |||
if (response.ok) { | |||
const jsonResponse = await response.json() | |||
const currentSurfData = await jsonResponse.hours.find(h => h.time.includes(currentISODate)) | |||
return displayStormglassData(currentSurfData) | |||
} | |||
return handleStormGlassApiError(response) | |||
} | |||
export default getStormglassData |
@ -0,0 +1,57 @@ | |||
// //////// STORMGLASS //////// // | |||
pp-stormglass | |||
pp-header | |||
p.header-spot | |||
pp-firstrow | |||
pp-watertemp | |||
p.watertemp-value | |||
p.watertemp-label water t° | |||
pp-winddir | |||
svg.winddir-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
width='24', | |||
height='24', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='3', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M12 5v14M19 12l-7 7-7-7') | |||
p.winddir-label wind dir. | |||
pp-windspeed | |||
p.windspeed-value | |||
p.windspeed-label wind spd. | |||
pp-linebreak | |||
pp-secondrow | |||
pp-waveheight | |||
p.waveheight-value | |||
p.waveheight-label wave hgt. | |||
pp-waveperiod | |||
p.waveperiod-value | |||
p.waveperiod-label wave per. | |||
pp-wavedir | |||
svg.wavedir-icon( | |||
xmlns='http://www.w3.org/2000/svg', | |||
width='24', | |||
height='24', | |||
fill='none', | |||
stroke='var(--main)', | |||
stroke-width='3', | |||
stroke-linecap='round', | |||
stroke-linejoin='round' | |||
) | |||
path(d='M12 5v14M19 12l-7 7-7-7') | |||
p.wavedir-label wave dir. | |||
pp-stormglass-loader-container | |||
pp-loader | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-stormglass-error-container | |||
p.error-notification API error | |||
p.stormglass-error-code |
@ -0,0 +1,49 @@ | |||
/************** UNSPLASH **************/ | |||
pp-unsplash { | |||
display: none; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
background-color: var(--myrtille-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
pp-unsplash > img { | |||
width: 100%; | |||
padding: var(--spacing-0000); | |||
border-radius: 12px; | |||
opacity: 0.5; | |||
filter: grayscale(80%); | |||
} | |||
pp-unsplash-loader-container { | |||
display: flex; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--pomme-background); | |||
border-radius: 10px; | |||
} | |||
pp-unsplash-error-container { | |||
display: none; | |||
flex-direction: column; | |||
grid-column: span 2; | |||
grid-row: span 4; | |||
justify-content: center; | |||
align-items: center; | |||
background-color: var(--main); | |||
border: 2px solid var(--search-background); | |||
border-radius: 10px; | |||
animation: module-display 500ms ease-in-out; | |||
} | |||
p.unsplash-error-code { | |||
margin-top: var(--spacing-0); | |||
color: var(--search-background); | |||
font-weight: var(--font-weight-bold); | |||
font-size: var(--scale-2); | |||
} |
@ -0,0 +1,41 @@ | |||
// //////// UNSPLASH //////// // | |||
const unsplashLoaderContainer = document.querySelector('pp-unsplash-loader-container') | |||
function handleUnsplashApiError(response) { | |||
const unsplashErrorContainer = document.querySelector('pp-unsplash-error-container') | |||
const unsplashErrorCode = document.querySelector('.unsplash-error-code') | |||
unsplashErrorCode.innerHTML = response.status | |||
unsplashLoaderContainer.style.display = 'none' | |||
unsplashErrorContainer.style.display = 'flex' | |||
} | |||
function displayUnsplashImage(data) { | |||
const image = document.querySelector('.unsplash-small') | |||
const unsplashLoader = document.querySelector('pp-unsplash-loader-container') | |||
const unsplashContainer = document.querySelector('pp-unsplash') | |||
image.src = data.urls.small | |||
unsplashLoader.style.display = 'none' | |||
unsplashContainer.style.display = 'flex' | |||
} | |||
async function getUnsplashData() { | |||
const apiKey = process.env.UNSPLASH_API_KEY | |||
const orientation = 'landscape' | |||
const collectionId = process.env.UNSPLASH_COLLECTION_ID | |||
const url = 'https://api.unsplash.com/' | |||
const path = 'photos/random/' | |||
const parameters = `client_id=${apiKey}&orientation=${orientation}&collections=${collectionId}` | |||
const response = await fetch(`${url}${path}?${parameters}`) | |||
if (response.ok) { | |||
const jsonResponse = await response.json() | |||
return displayUnsplashImage(jsonResponse) | |||
} | |||
return handleUnsplashApiError(response) | |||
} | |||
export default getUnsplashData |
@ -0,0 +1,15 @@ | |||
// //////// UNSPLASH //////// // | |||
pp-unsplash | |||
img.unsplash-small(src='') | |||
pp-unsplash-loader-container | |||
pp-loader | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-loader-dot | |||
pp-unsplash-error-container | |||
p.error-notification API error | |||
p.unsplash-error-code |
@ -0,0 +1,9 @@ | |||
[Unit] | |||
Description=Quark instance on 9999 for start page | |||
[Service] | |||
Type=simple | |||
ExecStart=/usr/local/bin/quark -p 9999 -d /home/yigit/.dotfiles/browser/startpage/dist -h 127.0.0.1 | |||
[Install] | |||
WantedBy=multi-user.target |
@ -0,0 +1,27 @@ | |||
ISC-License | |||
Copyright 2016-2021 Laslo Hunhold <dev@frign.de> | |||
Copyright 2004 Ted Unangst <tedu@openbsd.org> | |||
Copyright 2004 Todd C. Miller <Todd.Miller@courtesan.com> | |||
Copyright 2008 Otto Moerbeek <otto@drijf.net> | |||
Copyright 2017-2018 Hiltjo Posthuma <hiltjo@codemadness.org> | |||
Copyright 2017-2021 Quentin Rameau <quinq@fifth.space> | |||
Copyright 2018 Josuah Demangeon <mail@josuah.net> | |||
Copyright 2018 Dominik Schmidt <domischmidt@swissonline.ch> | |||
Copyright 2018 Aaron Burrow <burrows@charstarstar.com> | |||
Copyright 2020 Nihal Jere <nihal@nihaljere.xyz> | |||
Copyright 2020 Rainer Holzner <rholzner@web.de> | |||
Copyright 2020 Jeremy Bobbin <jer@jer.cx> | |||
Permission to use, copy, modify, and/or distribute this software for any | |||
purpose with or without fee is hereby granted, provided that the above | |||
copyright notice and this permission notice appear in all copies. | |||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
@ -0,0 +1,46 @@ | |||
# See LICENSE file for copyright and license details | |||
# quark - simple web server | |||
.POSIX: | |||
include config.mk | |||
COMPONENTS = connection data http queue server sock util | |||
all: quark | |||
connection.o: connection.c config.h connection.h data.h http.h server.h sock.h util.h config.mk | |||
data.o: data.c config.h data.h http.h server.h util.h config.mk | |||
http.o: http.c config.h http.h server.h util.h config.mk | |||
main.o: main.c arg.h config.h server.h sock.h util.h config.mk | |||
server.o: server.c config.h connection.h http.h queue.h server.h util.h config.mk | |||
sock.o: sock.c config.h sock.h util.h config.mk | |||
util.o: util.c config.h util.h config.mk | |||
quark: config.h $(COMPONENTS:=.o) $(COMPONENTS:=.h) main.o config.mk | |||
$(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(COMPONENTS:=.o) main.o $(LDFLAGS) | |||
config.h: | |||
cp config.def.h $@ | |||
clean: | |||
rm -f quark main.o $(COMPONENTS:=.o) | |||
dist: | |||
rm -rf "quark-$(VERSION)" | |||
mkdir -p "quark-$(VERSION)" | |||
cp -R LICENSE Makefile arg.h config.def.h config.mk quark.1 \ | |||
$(COMPONENTS:=.c) $(COMPONENTS:=.h) main.c "quark-$(VERSION)" | |||
tar -cf - "quark-$(VERSION)" | gzip -c > "quark-$(VERSION).tar.gz" | |||
rm -rf "quark-$(VERSION)" | |||
install: all | |||
mkdir -p "$(DESTDIR)$(PREFIX)/bin" | |||
cp -f quark "$(DESTDIR)$(PREFIX)/bin" | |||
chmod 755 "$(DESTDIR)$(PREFIX)/bin/quark" | |||
mkdir -p "$(DESTDIR)$(MANPREFIX)/man1" | |||
cp quark.1 "$(DESTDIR)$(MANPREFIX)/man1/quark.1" | |||
chmod 644 "$(DESTDIR)$(MANPREFIX)/man1/quark.1" | |||
uninstall: | |||
rm -f "$(DESTDIR)$(PREFIX)/bin/quark" | |||
rm -f "$(DESTDIR)$(MANPREFIX)/man1/quark.1" |
@ -0,0 +1,50 @@ | |||
/* | |||
* ISC-License | |||
* | |||
* Copyright 2004-2017 Christoph Lohmann <20h@r-36.net> | |||
* Copyright 2017-2018 Laslo Hunhold <dev@frign.de> | |||
* | |||
* Permission to use, copy, modify, and/or distribute this software for any | |||
* purpose with or without fee is hereby granted, provided that the above | |||
* copyright notice and this permission notice appear in all copies. | |||
* | |||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |||
*/ | |||
#ifndef ARG_H | |||
#define ARG_H | |||
extern char *argv0; | |||
/* int main(int argc, char *argv[]) */ | |||
#define ARGBEGIN for (argv0 = *argv, *argv ? (argc--, argv++) : ((void *)0); \ | |||
*argv && (*argv)[0] == '-' && (*argv)[1]; argc--, argv++) { \ | |||
int i_, argused_; \ | |||
if ((*argv)[1] == '-' && !(*argv)[2]) { \ | |||
argc--, argv++; \ | |||
break; \ | |||
} \ | |||
for (i_ = 1, argused_ = 0; (*argv)[i_]; i_++) { \ | |||
switch((*argv)[i_]) | |||
#define ARGEND if (argused_) { \ | |||
if ((*argv)[i_ + 1]) { \ | |||
break; \ | |||
} else { \ | |||
argc--, argv++; \ | |||
break; \ | |||
} \ | |||
} \ | |||
} \ | |||
} | |||
#define ARGC() ((*argv)[i_]) | |||
#define ARGF_(x) (((*argv)[i_ + 1]) ? (argused_ = 1, &((*argv)[i_ + 1])) : \ | |||
(*(argv + 1)) ? (argused_ = 1, *(argv + 1)) : (x)) | |||
#define EARGF(x) ARGF_(((x), exit(1), (char *)0)) | |||
#define ARGF() ARGF_((char *)0) | |||
#endif |
@ -0,0 +1,40 @@ | |||
#ifndef CONFIG_H | |||
#define CONFIG_H | |||
#define BUFFER_SIZE 4096 | |||
#define FIELD_MAX 200 | |||
/* mime-types */ | |||
static const struct { | |||
char *ext; | |||
char *type; | |||
} mimes[] = { | |||
{ "xml", "application/xml; charset=utf-8" }, | |||
{ "xhtml", "application/xhtml+xml; charset=utf-8" }, | |||
{ "html", "text/html; charset=utf-8" }, | |||
{ "js", "text/javascript; charset=utf-8" }, | |||
{ "htm", "text/html; charset=utf-8" }, | |||
{ "css", "text/css; charset=utf-8" }, | |||
{ "txt", "text/plain; charset=utf-8" }, | |||
{ "md", "text/plain; charset=utf-8" }, | |||
{ "c", "text/plain; charset=utf-8" }, | |||
{ "h", "text/plain; charset=utf-8" }, | |||
{ "gz", "application/x-gtar" }, | |||
{ "tar", "application/tar" }, | |||
{ "pdf", "application/x-pdf" }, | |||
{ "png", "image/png" }, | |||
{ "gif", "image/gif" }, | |||
{ "jpeg", "image/jpg" }, | |||
{ "jpg", "image/jpg" }, | |||
{ "iso", "application/x-iso9660-image" }, | |||
{ "webp", "image/webp" }, | |||
{ "svg", "image/svg+xml; charset=utf-8" }, | |||
{ "flac", "audio/flac" }, | |||
{ "mp3", "audio/mpeg" }, | |||
{ "ogg", "audio/ogg" }, | |||
{ "mp4", "video/mp4" }, | |||
{ "ogv", "video/ogg" }, | |||
{ "webm", "video/webm" }, | |||
}; | |||
#endif /* CONFIG_H */ |
@ -0,0 +1,16 @@ | |||
# quark version | |||
VERSION = 0 | |||
# Customize below to fit your system | |||
# paths | |||
PREFIX = /usr/local | |||
MANPREFIX = $(PREFIX)/share/man | |||
# flags | |||
CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -D_BSD_SOURCE | |||
CFLAGS = -std=c99 -pedantic -Wall -Wextra -Os | |||
LDFLAGS = -lpthread -s | |||
# compiler and linker | |||
CC = cc |
@ -0,0 +1,314 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <errno.h> | |||
#include <netinet/in.h> | |||
#include <stdio.h> | |||
#include <string.h> | |||
#include <sys/socket.h> | |||
#include <sys/types.h> | |||
#include <time.h> | |||
#include <unistd.h> | |||
#include "connection.h" | |||
#include "data.h" | |||
#include "http.h" | |||
#include "server.h" | |||
#include "sock.h" | |||
#include "util.h" | |||
struct worker_data { | |||
int insock; | |||
size_t nslots; | |||
const struct server *srv; | |||
}; | |||
void | |||
connection_log(const struct connection *c) | |||
{ | |||
char inaddr_str[INET6_ADDRSTRLEN /* > INET_ADDRSTRLEN */]; | |||
char tstmp[21]; | |||
/* create timestamp */ | |||
if (!strftime(tstmp, sizeof(tstmp), "%Y-%m-%dT%H:%M:%SZ", | |||
gmtime(&(time_t){time(NULL)}))) { | |||
warn("strftime: Exceeded buffer capacity"); | |||
/* continue anyway (we accept the truncation) */ | |||
} | |||
/* generate address-string */ | |||
if (sock_get_inaddr_str(&c->ia, inaddr_str, LEN(inaddr_str))) { | |||
warn("sock_get_inaddr_str: Couldn't generate adress-string"); | |||
inaddr_str[0] = '\0'; | |||
} | |||
printf("%s\t%s\t%s%.*d\t%s\t%s%s%s%s%s\n", | |||
tstmp, | |||
inaddr_str, | |||
(c->res.status == 0) ? "dropped" : "", | |||
(c->res.status == 0) ? 0 : 3, | |||
c->res.status, | |||
c->req.field[REQ_HOST][0] ? c->req.field[REQ_HOST] : "-", | |||
c->req.path[0] ? c->req.path : "-", | |||
c->req.query[0] ? "?" : "", | |||
c->req.query, | |||
c->req.fragment[0] ? "#" : "", | |||
c->req.fragment); | |||
} | |||
void | |||
connection_reset(struct connection *c) | |||
{ | |||
if (c != NULL) { | |||
shutdown(c->fd, SHUT_RDWR); | |||
close(c->fd); | |||
memset(c, 0, sizeof(*c)); | |||
} | |||
} | |||
void | |||
connection_serve(struct connection *c, const struct server *srv) | |||
{ | |||
enum status s; | |||
int done; | |||
switch (c->state) { | |||
case C_VACANT: | |||
/* | |||
* we were passed a "fresh" connection which should now | |||
* try to receive the header, reset buf beforehand | |||
*/ | |||
memset(&c->buf, 0, sizeof(c->buf)); | |||
c->state = C_RECV_HEADER; | |||
/* fallthrough */ | |||
case C_RECV_HEADER: | |||
/* receive header */ | |||
done = 0; | |||
if ((s = http_recv_header(c->fd, &c->buf, &done))) { | |||
http_prepare_error_response(&c->req, &c->res, s); | |||
goto response; | |||
} | |||
if (!done) { | |||
/* not done yet */ | |||
return; | |||
} | |||
/* parse header */ | |||
if ((s = http_parse_header(c->buf.data, &c->req))) { | |||
http_prepare_error_response(&c->req, &c->res, s); | |||
goto response; | |||
} | |||
/* prepare response struct */ | |||
http_prepare_response(&c->req, &c->res, srv); | |||
response: | |||
/* generate response header */ | |||
if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |||
http_prepare_error_response(&c->req, &c->res, s); | |||
if ((s = http_prepare_header_buf(&c->res, &c->buf))) { | |||
/* couldn't generate the header, we failed for good */ | |||
c->res.status = s; | |||
goto err; | |||
} | |||
} | |||
c->state = C_SEND_HEADER; | |||
/* fallthrough */ | |||
case C_SEND_HEADER: | |||
if ((s = http_send_buf(c->fd, &c->buf))) { | |||
c->res.status = s; | |||
goto err; | |||
} | |||
if (c->buf.len > 0) { | |||
/* not done yet */ | |||
return; | |||
} | |||
c->state = C_SEND_BODY; | |||
/* fallthrough */ | |||
case C_SEND_BODY: | |||
if (c->req.method == M_GET) { | |||
if (c->buf.len == 0) { | |||
/* fill buffer with body data */ | |||
if ((s = data_fct[c->res.type](&c->res, &c->buf, | |||
&c->progress))) { | |||
/* too late to do any real error handling */ | |||
c->res.status = s; | |||
goto err; | |||
} | |||
/* if the buffer remains empty, we are done */ | |||
if (c->buf.len == 0) { | |||
break; | |||
} | |||
} else { | |||
/* send buffer */ | |||
if ((s = http_send_buf(c->fd, &c->buf))) { | |||
/* too late to do any real error handling */ | |||
c->res.status = s; | |||
goto err; | |||
} | |||
} | |||
return; | |||
} | |||
break; | |||
default: | |||
warn("serve: invalid connection state"); | |||
return; | |||
} | |||
err: | |||
connection_log(c); | |||
connection_reset(c); | |||
} | |||
static struct connection * | |||
connection_get_drop_candidate(struct connection *connection, size_t nslots) | |||
{ | |||
struct connection *c, *minc; | |||
size_t i, j, maxcnt, cnt; | |||
/* | |||
* determine the most-unimportant connection 'minc' of the in-address | |||
* with most connections; this algorithm has a complexity of O(n²) | |||
* in time but is O(1) in space; there are algorithms with O(n) in | |||
* time and space, but this would require memory allocation, | |||
* which we avoid. Given the simplicity of the inner loop and | |||
* relatively small number of slots per thread, this is fine. | |||
*/ | |||
for (i = 0, minc = NULL, maxcnt = 0; i < nslots; i++) { | |||
/* | |||
* we determine how many connections have the same | |||
* in-address as connection[i], but also minimize over | |||
* that set with other criteria, yielding a general | |||
* minimizer c. We first set it to connection[i] and | |||
* update it, if a better candidate shows up, in the inner | |||
* loop | |||
*/ | |||
c = &connection[i]; | |||
for (j = 0, cnt = 0; j < nslots; j++) { | |||
if (!sock_same_addr(&connection[i].ia, | |||
&connection[j].ia)) { | |||
continue; | |||
} | |||
cnt++; | |||
/* minimize over state */ | |||
if (connection[j].state < c->state) { | |||
c = &connection[j]; | |||
} else if (connection[j].state == c->state) { | |||
/* minimize over progress */ | |||
if (c->state == C_SEND_BODY && | |||
connection[i].res.type != c->res.type) { | |||
/* | |||
* mixed response types; progress | |||
* is not comparable | |||
* | |||
* the res-type-enum is ordered as | |||
* DIRLISTING, ERROR, FILE, i.e. | |||
* in rising priority, because a | |||
* file transfer is most important, | |||
* followed by error-messages. | |||
* Dirlistings as an "interactive" | |||
* feature (that take up lots of | |||
* resources) have the lowest | |||
* priority | |||
*/ | |||
if (connection[i].res.type < | |||
c->res.type) { | |||
c = &connection[j]; | |||
} | |||
} else if (connection[j].progress < | |||
c->progress) { | |||
/* | |||
* for C_SEND_BODY with same response | |||
* type, C_RECV_HEADER and C_SEND_BODY | |||
* it is sufficient to compare the | |||
* raw progress | |||
*/ | |||
c = &connection[j]; | |||
} | |||
} | |||
} | |||
if (cnt > maxcnt) { | |||
/* this run yielded an even greedier in-address */ | |||
minc = c; | |||
maxcnt = cnt; | |||
} | |||
} | |||
return minc; | |||
} | |||
struct connection * | |||
connection_accept(int insock, struct connection *connection, size_t nslots) | |||
{ | |||
struct connection *c = NULL; | |||
size_t i; | |||
/* find vacant connection (i.e. one with no fd assigned to it) */ | |||
for (i = 0; i < nslots; i++) { | |||
if (connection[i].fd == 0) { | |||
c = &connection[i]; | |||
break; | |||
} | |||
} | |||
if (i == nslots) { | |||
/* | |||
* all our connection-slots are occupied and the only | |||
* way out is to drop another connection, because not | |||
* accepting this connection just kicks this can further | |||
* down the road (to the next queue_wait()) without | |||
* solving anything. | |||
* | |||
* This may sound bad, but this case can only be hit | |||
* either when there's a (D)DoS-attack or a massive | |||
* influx of requests. The latter is impossible to solve | |||
* at this moment without expanding resources, but the | |||
* former has certain characteristics allowing us to | |||
* handle this gracefully. | |||
* | |||
* During an attack (e.g. Slowloris, R-U-Dead-Yet, Slow | |||
* Read or just plain flooding) we can not see who is | |||
* waiting to be accept()ed. | |||
* However, an attacker usually already has many | |||
* connections open (while well-behaved clients could | |||
* do everything with just one connection using | |||
* keep-alive). Inferring a likely attacker-connection | |||
* is an educated guess based on which in-address is | |||
* occupying the most connection slots. Among those, | |||
* connections in early stages (receiving or sending | |||
* headers) are preferred over connections in late | |||
* stages (sending body). | |||
* | |||
* This quantitative approach effectively drops malicious | |||
* connections while preserving even long-running | |||
* benevolent connections like downloads. | |||
*/ | |||
c = connection_get_drop_candidate(connection, nslots); | |||
c->res.status = 0; | |||
connection_log(c); | |||
connection_reset(c); | |||
} | |||
/* accept connection */ | |||
if ((c->fd = accept(insock, (struct sockaddr *)&c->ia, | |||
&(socklen_t){sizeof(c->ia)})) < 0) { | |||
if (errno != EAGAIN && errno != EWOULDBLOCK) { | |||
/* | |||
* this should not happen, as we received the | |||
* event that there are pending connections here | |||
*/ | |||
warn("accept:"); | |||
} | |||
return NULL; | |||
} | |||
/* set socket to non-blocking mode */ | |||
if (sock_set_nonblocking(c->fd)) { | |||
/* we can't allow blocking sockets */ | |||
return NULL; | |||
} | |||
return c; | |||
} |
@ -0,0 +1,32 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef CONNECTION_H | |||
#define CONNECTION_H | |||
#include "http.h" | |||
#include "server.h" | |||
#include "util.h" | |||
enum connection_state { | |||
C_VACANT, | |||
C_RECV_HEADER, | |||
C_SEND_HEADER, | |||
C_SEND_BODY, | |||
NUM_CONN_STATES, | |||
}; | |||
struct connection { | |||
enum connection_state state; | |||
int fd; | |||
struct sockaddr_storage ia; | |||
struct request req; | |||
struct response res; | |||
struct buffer buf; | |||
size_t progress; | |||
}; | |||
struct connection *connection_accept(int, struct connection *, size_t); | |||
void connection_log(const struct connection *); | |||
void connection_reset(struct connection *); | |||
void connection_serve(struct connection *, const struct server *); | |||
#endif /* CONNECTION_H */ |
@ -0,0 +1,231 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <dirent.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <sys/stat.h> | |||
#include <time.h> | |||
#include <unistd.h> | |||
#include "data.h" | |||
#include "http.h" | |||
#include "util.h" | |||
enum status (* const data_fct[])(const struct response *, | |||
struct buffer *, size_t *) = { | |||
[RESTYPE_DIRLISTING] = data_prepare_dirlisting_buf, | |||
[RESTYPE_ERROR] = data_prepare_error_buf, | |||
[RESTYPE_FILE] = data_prepare_file_buf, | |||
}; | |||
static int | |||
compareent(const struct dirent **d1, const struct dirent **d2) | |||
{ | |||
int v; | |||
v = ((*d2)->d_type == DT_DIR ? 1 : -1) - | |||
((*d1)->d_type == DT_DIR ? 1 : -1); | |||
if (v) { | |||
return v; | |||
} | |||
return strcmp((*d1)->d_name, (*d2)->d_name); | |||
} | |||
static char * | |||
suffix(int t) | |||
{ | |||
switch (t) { | |||
case DT_FIFO: return "|"; | |||
case DT_DIR: return "/"; | |||
case DT_LNK: return "@"; | |||
case DT_SOCK: return "="; | |||
} | |||
return ""; | |||
} | |||
static void | |||
html_escape(const char *src, char *dst, size_t dst_siz) | |||
{ | |||
const struct { | |||
char c; | |||
char *s; | |||
} escape[] = { | |||
{ '&', "&" }, | |||
{ '<', "<" }, | |||
{ '>', ">" }, | |||
{ '"', """ }, | |||
{ '\'', "'" }, | |||
}; | |||
size_t i, j, k, esclen; | |||
for (i = 0, j = 0; src[i] != '\0'; i++) { | |||
for (k = 0; k < LEN(escape); k++) { | |||
if (src[i] == escape[k].c) { | |||
break; | |||
} | |||
} | |||
if (k == LEN(escape)) { | |||
/* no escape char at src[i] */ | |||
if (j == dst_siz - 1) { | |||
/* silent truncation */ | |||
break; | |||
} else { | |||
dst[j++] = src[i]; | |||
} | |||
} else { | |||
/* escape char at src[i] */ | |||
esclen = strlen(escape[k].s); | |||
if (j >= dst_siz - esclen) { | |||
/* silent truncation */ | |||
break; | |||
} else { | |||
memcpy(&dst[j], escape[k].s, esclen); | |||
j += esclen; | |||
} | |||
} | |||
} | |||
dst[j] = '\0'; | |||
} | |||
enum status | |||
data_prepare_dirlisting_buf(const struct response *res, | |||
struct buffer *buf, size_t *progress) | |||
{ | |||
enum status s = 0; | |||
struct dirent **e; | |||
size_t i; | |||
int dirlen; | |||
char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */ | |||
/* reset buffer */ | |||
memset(buf, 0, sizeof(*buf)); | |||
/* read directory */ | |||
if ((dirlen = scandir(res->internal_path, &e, NULL, compareent)) < 0) { | |||
return S_FORBIDDEN; | |||
} | |||
if (*progress == 0) { | |||
/* write listing header (sizeof(esc) >= PATH_MAX) */ | |||
html_escape(res->path, esc, MIN(PATH_MAX, sizeof(esc))); | |||
if (buffer_appendf(buf, | |||
"<!DOCTYPE html>\n<html>\n\t<head>" | |||
"<title>Index of %s</title></head>\n" | |||
"\t<body>\n\t\t<a href=\"..\">..</a>", | |||
esc) < 0) { | |||
s = S_REQUEST_TIMEOUT; | |||
goto cleanup; | |||
} | |||
} | |||
/* listing entries */ | |||
for (i = *progress; i < (size_t)dirlen; i++) { | |||
/* skip hidden files, "." and ".." */ | |||
if (e[i]->d_name[0] == '.') { | |||
continue; | |||
} | |||
/* entry line */ | |||
html_escape(e[i]->d_name, esc, sizeof(esc)); | |||
if (buffer_appendf(buf, | |||
"<br />\n\t\t<a href=\"%s%s\">%s%s</a>", | |||
esc, | |||
(e[i]->d_type == DT_DIR) ? "/" : "", | |||
esc, | |||
suffix(e[i]->d_type))) { | |||
/* buffer full */ | |||
break; | |||
} | |||
} | |||
*progress = i; | |||
if (*progress == (size_t)dirlen) { | |||
/* listing footer */ | |||
if (buffer_appendf(buf, "\n\t</body>\n</html>\n") < 0) { | |||
s = S_REQUEST_TIMEOUT; | |||
goto cleanup; | |||
} | |||
(*progress)++; | |||
} | |||
cleanup: | |||
while (dirlen--) { | |||
free(e[dirlen]); | |||
} | |||
free(e); | |||
return s; | |||
} | |||
enum status | |||
data_prepare_error_buf(const struct response *res, struct buffer *buf, | |||
size_t *progress) | |||
{ | |||
/* reset buffer */ | |||
memset(buf, 0, sizeof(*buf)); | |||
if (*progress == 0) { | |||
/* write error body */ | |||
if (buffer_appendf(buf, | |||
"<!DOCTYPE html>\n<html>\n\t<head>\n" | |||
"\t\t<title>%d %s</title>\n\t</head>\n" | |||
"\t<body>\n\t\t<h1>%d %s</h1>\n" | |||
"\t</body>\n</html>\n", | |||
res->status, status_str[res->status], | |||
res->status, status_str[res->status])) { | |||
return S_INTERNAL_SERVER_ERROR; | |||
} | |||
(*progress)++; | |||
} | |||
return 0; | |||
} | |||
enum status | |||
data_prepare_file_buf(const struct response *res, struct buffer *buf, | |||
size_t *progress) | |||
{ | |||
FILE *fp; | |||
enum status s = 0; | |||
ssize_t r; | |||
size_t remaining; | |||
/* reset buffer */ | |||
memset(buf, 0, sizeof(*buf)); | |||
/* open file */ | |||
if (!(fp = fopen(res->internal_path, "r"))) { | |||
s = S_FORBIDDEN; | |||
goto cleanup; | |||
} | |||
/* seek to lower bound + progress */ | |||
if (fseek(fp, res->file.lower + *progress, SEEK_SET)) { | |||
s = S_INTERNAL_SERVER_ERROR; | |||
goto cleanup; | |||
} | |||
/* read data into buf */ | |||
remaining = res->file.upper - res->file.lower + 1 - *progress; | |||
while ((r = fread(buf->data + buf->len, 1, | |||
MIN(sizeof(buf->data) - buf->len, | |||
remaining), fp))) { | |||
if (r < 0) { | |||
s = S_INTERNAL_SERVER_ERROR; | |||
goto cleanup; | |||
} | |||
buf->len += r; | |||
*progress += r; | |||
remaining -= r; | |||
} | |||
cleanup: | |||
if (fp) { | |||
fclose(fp); | |||
} | |||
return s; | |||
} |
@ -0,0 +1,18 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef DATA_H | |||
#define DATA_H | |||
#include "http.h" | |||
#include "util.h" | |||
extern enum status (* const data_fct[])(const struct response *, | |||
struct buffer *, size_t *); | |||
enum status data_prepare_dirlisting_buf(const struct response *, | |||
struct buffer *, size_t *); | |||
enum status data_prepare_error_buf(const struct response *, | |||
struct buffer *, size_t *); | |||
enum status data_prepare_file_buf(const struct response *, | |||
struct buffer *, size_t *); | |||
#endif /* DATA_H */ |
@ -0,0 +1,97 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef HTTP_H | |||
#define HTTP_H | |||
#include <limits.h> | |||
#include <sys/socket.h> | |||
#include "config.h" | |||
#include "server.h" | |||
#include "util.h" | |||
enum req_field { | |||
REQ_HOST, | |||
REQ_RANGE, | |||
REQ_IF_MODIFIED_SINCE, | |||
NUM_REQ_FIELDS, | |||
}; | |||
extern const char *req_field_str[]; | |||
enum req_method { | |||
M_GET, | |||
M_HEAD, | |||
NUM_REQ_METHODS, | |||
}; | |||
extern const char *req_method_str[]; | |||
struct request { | |||
enum req_method method; | |||
char path[PATH_MAX]; | |||
char query[FIELD_MAX]; | |||
char fragment[FIELD_MAX]; | |||
char field[NUM_REQ_FIELDS][FIELD_MAX]; | |||
}; | |||
enum status { | |||
S_OK = 200, | |||
S_PARTIAL_CONTENT = 206, | |||
S_MOVED_PERMANENTLY = 301, | |||
S_NOT_MODIFIED = 304, | |||
S_BAD_REQUEST = 400, | |||
S_FORBIDDEN = 403, | |||
S_NOT_FOUND = 404, | |||
S_METHOD_NOT_ALLOWED = 405, | |||
S_REQUEST_TIMEOUT = 408, | |||
S_RANGE_NOT_SATISFIABLE = 416, | |||
S_REQUEST_TOO_LARGE = 431, | |||
S_INTERNAL_SERVER_ERROR = 500, | |||
S_VERSION_NOT_SUPPORTED = 505, | |||
}; | |||
extern const char *status_str[]; | |||
enum res_field { | |||
RES_ACCEPT_RANGES, | |||
RES_ALLOW, | |||
RES_LOCATION, | |||
RES_LAST_MODIFIED, | |||
RES_CONTENT_LENGTH, | |||
RES_CONTENT_RANGE, | |||
RES_CONTENT_TYPE, | |||
NUM_RES_FIELDS, | |||
}; | |||
extern const char *res_field_str[]; | |||
enum res_type { | |||
RESTYPE_DIRLISTING, | |||
RESTYPE_ERROR, | |||
RESTYPE_FILE, | |||
NUM_RES_TYPES, | |||
}; | |||
struct response { | |||
enum res_type type; | |||
enum status status; | |||
char field[NUM_RES_FIELDS][FIELD_MAX]; | |||
char path[PATH_MAX]; | |||
char internal_path[PATH_MAX]; | |||
struct vhost *vhost; | |||
struct { | |||
size_t lower; | |||
size_t upper; | |||
} file; | |||
}; | |||
enum status http_prepare_header_buf(const struct response *, struct buffer *); | |||
enum status http_send_buf(int, struct buffer *); | |||
enum status http_recv_header(int, struct buffer *, int *); | |||
enum status http_parse_header(const char *, struct request *); | |||
void http_prepare_response(const struct request *, struct response *, | |||
const struct server *); | |||
void http_prepare_error_response(const struct request *, | |||
struct response *, enum status); | |||
#endif /* HTTP_H */ |
@ -0,0 +1,364 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <errno.h> | |||
#include <grp.h> | |||
#include <limits.h> | |||
#include <pwd.h> | |||
#include <regex.h> | |||
#include <signal.h> | |||
#include <stddef.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <sys/resource.h> | |||
#include <sys/time.h> | |||
#include <sys/types.h> | |||
#include <sys/wait.h> | |||
#include <unistd.h> | |||
#include "arg.h" | |||
#include "server.h" | |||
#include "sock.h" | |||
#include "util.h" | |||
static char *udsname; | |||
static void | |||
cleanup(void) | |||
{ | |||
if (udsname) { | |||
sock_rem_uds(udsname); | |||
} | |||
} | |||
static void | |||
sigcleanup(int sig) | |||
{ | |||
cleanup(); | |||
kill(0, sig); | |||
_exit(1); | |||
} | |||
static void | |||
handlesignals(void(*hdl)(int)) | |||
{ | |||
struct sigaction sa = { | |||
.sa_handler = hdl, | |||
}; | |||
sigemptyset(&sa.sa_mask); | |||
sigaction(SIGTERM, &sa, NULL); | |||
sigaction(SIGHUP, &sa, NULL); | |||
sigaction(SIGINT, &sa, NULL); | |||
sigaction(SIGQUIT, &sa, NULL); | |||
} | |||
static void | |||
usage(void) | |||
{ | |||
const char *opts = "[-u user] [-g group] [-n num] [-d dir] [-l] " | |||
"[-i file] [-v vhost] ... [-m map] ..."; | |||
die("usage: %s -p port [-h host] %s\n" | |||
" %s -U file [-p port] %s", argv0, | |||
opts, argv0, opts); | |||
} | |||
int | |||
main(int argc, char *argv[]) | |||
{ | |||
struct group *grp = NULL; | |||
struct passwd *pwd = NULL; | |||
struct rlimit rlim; | |||
struct server srv = { | |||
.docindex = "index.html", | |||
}; | |||
size_t i; | |||
int insock, status = 0; | |||
const char *err; | |||
char *tok[4]; | |||
/* defaults */ | |||
size_t nthreads = 4; | |||
size_t nslots = 64; | |||
char *servedir = "."; | |||
char *user = "nobody"; | |||
char *group = "nogroup"; | |||
ARGBEGIN { | |||
case 'd': | |||
servedir = EARGF(usage()); | |||
break; | |||
case 'g': | |||
group = EARGF(usage()); | |||
break; | |||
case 'h': | |||
srv.host = EARGF(usage()); | |||
break; | |||
case 'i': | |||
srv.docindex = EARGF(usage()); | |||
if (strchr(srv.docindex, '/')) { | |||
die("The document index must not contain '/'"); | |||
} | |||
break; | |||
case 'l': | |||
srv.listdirs = 1; | |||
break; | |||
case 'm': | |||
if (spacetok(EARGF(usage()), tok, 3) || !tok[0] || !tok[1]) { | |||
usage(); | |||
} | |||
if (!(srv.map = reallocarray(srv.map, ++srv.map_len, | |||
sizeof(struct map)))) { | |||
die("reallocarray:"); | |||
} | |||
srv.map[srv.map_len - 1].from = tok[0]; | |||
srv.map[srv.map_len - 1].to = tok[1]; | |||
srv.map[srv.map_len - 1].chost = tok[2]; | |||
break; | |||
case 's': | |||
err = NULL; | |||
nslots = strtonum(EARGF(usage()), 1, INT_MAX, &err); | |||
if (err) { | |||
die("strtonum '%s': %s", EARGF(usage()), err); | |||
} | |||
break; | |||
case 't': | |||
err = NULL; | |||
nthreads = strtonum(EARGF(usage()), 1, INT_MAX, &err); | |||
if (err) { | |||
die("strtonum '%s': %s", EARGF(usage()), err); | |||
} | |||
break; | |||
case 'p': | |||
srv.port = EARGF(usage()); | |||
break; | |||
case 'U': | |||
udsname = EARGF(usage()); | |||
break; | |||
case 'u': | |||
user = EARGF(usage()); | |||
break; | |||
case 'v': | |||
if (spacetok(EARGF(usage()), tok, 4) || !tok[0] || !tok[1] || | |||
!tok[2]) { | |||
usage(); | |||
} | |||
if (!(srv.vhost = reallocarray(srv.vhost, ++srv.vhost_len, | |||
sizeof(*srv.vhost)))) { | |||
die("reallocarray:"); | |||
} | |||
srv.vhost[srv.vhost_len - 1].chost = tok[0]; | |||
srv.vhost[srv.vhost_len - 1].regex = tok[1]; | |||
srv.vhost[srv.vhost_len - 1].dir = tok[2]; | |||
srv.vhost[srv.vhost_len - 1].prefix = tok[3]; | |||
break; | |||
default: | |||
usage(); | |||
} ARGEND | |||
if (argc) { | |||
usage(); | |||
} | |||
/* can't have both host and UDS but must have one of port or UDS*/ | |||
if ((srv.host && udsname) || !(srv.port || udsname)) { | |||
usage(); | |||
} | |||
if (udsname && (!access(udsname, F_OK) || errno != ENOENT)) { | |||
die("UNIX-domain socket '%s': %s", udsname, errno ? | |||
strerror(errno) : "File exists"); | |||
} | |||
/* compile and check the supplied vhost regexes */ | |||
for (i = 0; i < srv.vhost_len; i++) { | |||
if (regcomp(&srv.vhost[i].re, srv.vhost[i].regex, | |||
REG_EXTENDED | REG_ICASE | REG_NOSUB)) { | |||
die("regcomp '%s': invalid regex", | |||
srv.vhost[i].regex); | |||
} | |||
} | |||
/* validate user and group */ | |||
errno = 0; | |||
if (!user || !(pwd = getpwnam(user))) { | |||
die("getpwnam '%s': %s", user ? user : "null", | |||
errno ? strerror(errno) : "Entry not found"); | |||
} | |||
errno = 0; | |||
if (!group || !(grp = getgrnam(group))) { | |||
die("getgrnam '%s': %s", group ? group : "null", | |||
errno ? strerror(errno) : "Entry not found"); | |||
} | |||
/* open a new process group */ | |||
setpgid(0, 0); | |||
handlesignals(sigcleanup); | |||
/* | |||
* set the maximum number of open file descriptors as needed | |||
* - 3 initial fd's | |||
* - nthreads fd's for the listening socket | |||
* - (nthreads * nslots) fd's for the connection-fd | |||
* - (5 * nthreads) fd's for general purpose thread-use | |||
*/ | |||
rlim.rlim_cur = rlim.rlim_max = 3 + nthreads + nthreads * nslots + | |||
5 * nthreads; | |||
if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) { | |||
if (errno == EPERM) { | |||
die("You need to run as root or have " | |||
"CAP_SYS_RESOURCE set, or are asking for more " | |||
"file descriptors than the system can offer"); | |||
} else { | |||
die("setrlimit:"); | |||
} | |||
} | |||
/* | |||
* create the (non-blocking) listening socket | |||
* | |||
* we could use SO_REUSEPORT and create a listening socket for | |||
* each thread (for better load-balancing, given each thread | |||
* would get his own kernel-queue), but this increases latency | |||
* (as a thread might get stuck on a larger request, making all | |||
* other request wait in line behind it). | |||
* | |||
* socket contention with a single listening socket is a | |||
* non-issue and thread-load-balancing is better fixed in the | |||
* kernel by changing epoll-sheduling from a FIFO- to a | |||
* LIFO-model, especially as it doesn't affect performance | |||
*/ | |||
insock = udsname ? sock_get_uds(udsname, pwd->pw_uid, grp->gr_gid) : | |||
sock_get_ips(srv.host, srv.port); | |||
if (sock_set_nonblocking(insock)) { | |||
return 1; | |||
} | |||
/* | |||
* before dropping privileges, we fork, as we need to remove | |||
* the UNIX-domain socket when we shut down, which we need | |||
* privileges for | |||
*/ | |||
switch (fork()) { | |||
case -1: | |||
warn("fork:"); | |||
break; | |||
case 0: | |||
/* restore default handlers */ | |||
handlesignals(SIG_DFL); | |||
/* reap children automatically */ | |||
if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { | |||
die("signal: Failed to set SIG_IGN on SIGCHLD"); | |||
} | |||
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR) { | |||
die("signal: Failed to set SIG_IGN on SIGPIPE"); | |||
} | |||
/* | |||
* try increasing the thread-limit by the number | |||
* of threads we need (which is the only reliable | |||
* workaround I know given the thread-limit is per user | |||
* rather than per process), but ignore EPERM errors, | |||
* because this most probably means the user has already | |||
* set the value to the kernel's limit, and there's not | |||
* much we can do in any other case. | |||
* There's also no danger of overflow as the value | |||
* returned by getrlimit() is way below the limits of the | |||
* rlim_t datatype. | |||
*/ | |||
if (getrlimit(RLIMIT_NPROC, &rlim) < 0) { | |||
die("getrlimit:"); | |||
} | |||
if (rlim.rlim_max == RLIM_INFINITY) { | |||
if (rlim.rlim_cur != RLIM_INFINITY) { | |||
/* try increasing current limit by nthreads */ | |||
rlim.rlim_cur += nthreads; | |||
} | |||
} else { | |||
/* try increasing current and hard limit by nthreads */ | |||
rlim.rlim_cur = rlim.rlim_max += nthreads; | |||
} | |||
if (setrlimit(RLIMIT_NPROC, &rlim) < 0 && errno != EPERM) { | |||
die("setrlimit()"); | |||
} | |||
/* limit ourselves to reading the servedir and block further unveils */ | |||
eunveil(servedir, "r"); | |||
eunveil(NULL, NULL); | |||
/* chroot */ | |||
if (chdir(servedir) < 0) { | |||
die("chdir '%s':", servedir); | |||
} | |||
if (chroot(".") < 0) { | |||
if (errno == EPERM) { | |||
die("You need to run as root or have " | |||
"CAP_SYS_CHROOT set"); | |||
} else { | |||
die("chroot:"); | |||
} | |||
} | |||
/* drop root */ | |||
if (pwd->pw_uid == 0 || grp->gr_gid == 0) { | |||
die("Won't run under root %s for hopefully obvious reasons", | |||
(pwd->pw_uid == 0) ? (grp->gr_gid == 0) ? | |||
"user and group" : "user" : "group"); | |||
} | |||
if (setgroups(1, &(grp->gr_gid)) < 0) { | |||
if (errno == EPERM) { | |||
die("You need to run as root or have " | |||
"CAP_SETGID set"); | |||
} else { | |||
die("setgroups:"); | |||
} | |||
} | |||
if (setgid(grp->gr_gid) < 0) { | |||
if (errno == EPERM) { | |||
die("You need to run as root or have " | |||
"CAP_SETGID set"); | |||
} else { | |||
die("setgid:"); | |||
} | |||
} | |||
if (setuid(pwd->pw_uid) < 0) { | |||
if (errno == EPERM) { | |||
die("You need to run as root or have " | |||
"CAP_SETUID set"); | |||
} else { | |||
die("setuid:"); | |||
} | |||
} | |||
if (udsname) { | |||
epledge("stdio rpath proc unix", NULL); | |||
} else { | |||
epledge("stdio rpath proc inet", NULL); | |||
} | |||
/* accept incoming connections */ | |||
server_init_thread_pool(insock, nthreads, nslots, &srv); | |||
exit(0); | |||
default: | |||
/* limit ourselves even further while we are waiting */ | |||
if (udsname) { | |||
eunveil(udsname, "c"); | |||
eunveil(NULL, NULL); | |||
epledge("stdio cpath", NULL); | |||
} else { | |||
eunveil("/", ""); | |||
eunveil(NULL, NULL); | |||
epledge("stdio", NULL); | |||
} | |||
while (wait(&status) > 0) | |||
; | |||
} | |||
cleanup(); | |||
return status; | |||
} |
@ -0,0 +1,137 @@ | |||
.Dd 2020-09-27 | |||
.Dt QUARK 1 | |||
.Os suckless.org | |||
.Sh NAME | |||
.Nm quark | |||
.Nd simple static web server | |||
.Sh SYNOPSIS | |||
.Nm | |||
.Fl p Ar port | |||
.Op Fl h Ar host | |||
.Op Fl u Ar user | |||
.Op Fl g Ar group | |||
.Op Fl s Ar num | |||
.Op Fl t Ar num | |||
.Op Fl d Ar dir | |||
.Op Fl l | |||
.Op Fl i Ar file | |||
.Oo Fl v Ar vhost Oc ... | |||
.Oo Fl m Ar map Oc ... | |||
.Nm | |||
.Fl U Ar file | |||
.Op Fl p Ar port | |||
.Op Fl u Ar user | |||
.Op Fl g Ar group | |||
.Op Fl s Ar num | |||
.Op Fl t Ar num | |||
.Op Fl d Ar dir | |||
.Op Fl l | |||
.Op Fl i Ar file | |||
.Oo Fl v Ar vhost Oc ... | |||
.Oo Fl m Ar map Oc ... | |||
.Sh DESCRIPTION | |||
.Nm | |||
is a simple HTTP GET/HEAD-only web server for static content. | |||
It supports virtual hosts (see | |||
.Fl v ) , | |||
explicit redirects (see | |||
.Fl m ) , | |||
directory listings (see | |||
.Fl l ) , | |||
conditional "If-Modified-Since"-requests (RFC 7232), range requests | |||
(RFC 7233) and well-known URIs (RFC 8615), while refusing to serve | |||
hidden files and directories. | |||
.Sh OPTIONS | |||
.Bl -tag -width Ds | |||
.It Fl d Ar dir | |||
Serve | |||
.Ar dir | |||
after chrooting into it. | |||
The default is ".". | |||
.It Fl g Ar group | |||
Set group ID when dropping privileges, and in socket mode the group of the | |||
socket file, to the ID of | |||
.Ar group . | |||
The default is "nogroup". | |||
.It Fl h Ar host | |||
Use | |||
.Ar host | |||
as the server hostname. | |||
The default is the loopback interface (i.e. localhost). | |||
.It Fl i Ar file | |||
Set | |||
.Ar file | |||
as the directory index. | |||
The default is "index.html". | |||
.It Fl l | |||
Enable directory listing. | |||
.It Fl m Ar map | |||
Add the URI prefix mapping rule specified by | |||
.Ar map , | |||
which has the form | |||
.Qq Pa from to [chost] , | |||
where each element is separated with spaces (0x20) that can be | |||
escaped with '\\'. | |||
.Pp | |||
The prefix | |||
.Pa from | |||
of all matching URIs is replaced with | |||
.Pa to , | |||
optionally limited to the canonical virtual host | |||
.Pa chost . | |||
If no virtual hosts are given, | |||
.Pa chost | |||
is ignored. | |||
.It Fl p Ar port | |||
In host mode, listen on port | |||
.Ar port | |||
for incoming connections. | |||
In socket mode, use | |||
.Ar port | |||
for constructing proper virtual host | |||
redirects on non-standard ports. | |||
.It Fl U Ar file | |||
Create the UNIX-domain socket | |||
.Ar file , | |||
listen on it for incoming connections and remove it on exit. | |||
.It Fl s Ar num | |||
Set the number of connection slots per worker thread to | |||
.Ar num . | |||
The default is 64. | |||
.It Fl t Ar num | |||
Set the number of worker threads to | |||
.Ar num . | |||
The default is 4. | |||
.It Fl u Ar user | |||
Set user ID when dropping privileges, | |||
and in socket mode the user of the socket file, | |||
to the ID of | |||
.Ar user . | |||
The default is "nobody". | |||
.It Fl v Ar vhost | |||
Add the virtual host specified by | |||
.Ar vhost , | |||
which has the form | |||
.Qq Pa chost regex dir [prefix] , | |||
where each element is separated with spaces (0x20) that can be | |||
escaped with '\\'. | |||
.Pp | |||
A request matching the virtual host regular expression | |||
.Pa regex | |||
(see | |||
.Xr regex 3 ) | |||
is redirected to the canonical host | |||
.Pa chost , | |||
if they differ, using the directory | |||
.Pa dir | |||
as the root directory, optionally prefixing the URI with | |||
.Pa prefix . | |||
If any virtual hosts are specified, all requests on non-matching | |||
hosts are discarded. | |||
.El | |||
.Sh CUSTOMIZATION | |||
.Nm | |||
can be customized by creating a custom config.h from config.def.h and | |||
(re)compiling the source code. This keeps it fast, secure and simple. | |||
.Sh AUTHORS | |||
.An Laslo Hunhold Aq Mt dev@frign.de |
@ -0,0 +1,217 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <stddef.h> | |||
#ifdef __linux__ | |||
#include <sys/epoll.h> | |||
#else | |||
#include <sys/types.h> | |||
#include <sys/event.h> | |||
#include <sys/time.h> | |||
#endif | |||
#include "queue.h" | |||
#include "util.h" | |||
int | |||
queue_create(void) | |||
{ | |||
int qfd; | |||
#ifdef __linux__ | |||
if ((qfd = epoll_create1(0)) < 0) { | |||
warn("epoll_create1:"); | |||
} | |||
#else | |||
if ((qfd = kqueue()) < 0) { | |||
warn("kqueue:"); | |||
} | |||
#endif | |||
return qfd; | |||
} | |||
int | |||
queue_add_fd(int qfd, int fd, enum queue_event_type t, int shared, | |||
const void *data) | |||
{ | |||
#ifdef __linux__ | |||
struct epoll_event e; | |||
/* set event flag */ | |||
if (shared) { | |||
/* | |||
* if the fd is shared, "exclusive" is the only | |||
* way to avoid spurious wakeups and "blocking" | |||
* accept()'s. | |||
*/ | |||
e.events = EPOLLEXCLUSIVE; | |||
} else { | |||
/* | |||
* if we have the fd for ourselves (i.e. only | |||
* within the thread), we want to be | |||
* edge-triggered, as our logic makes sure | |||
* that the buffers are drained when we return | |||
* to epoll_wait() | |||
*/ | |||
e.events = EPOLLET; | |||
} | |||
switch (t) { | |||
case QUEUE_EVENT_IN: | |||
e.events |= EPOLLIN; | |||
break; | |||
case QUEUE_EVENT_OUT: | |||
e.events |= EPOLLOUT; | |||
break; | |||
} | |||
/* set data pointer */ | |||
e.data.ptr = (void *)data; | |||
/* register fd in the interest list */ | |||
if (epoll_ctl(qfd, EPOLL_CTL_ADD, fd, &e) < 0) { | |||
warn("epoll_ctl:"); | |||
return -1; | |||
} | |||
#else | |||
struct kevent e; | |||
int events; | |||
/* prepare event flag */ | |||
events = (shared) ? 0 : EV_CLEAR; | |||
switch (t) { | |||
case QUEUE_EVENT_IN: | |||
events |= EVFILT_READ; | |||
break; | |||
case QUEUE_EVENT_OUT: | |||
events |= EVFILT_WRITE; | |||
break; | |||
} | |||
EV_SET(&e, fd, events, EV_ADD, 0, 0, (void *)data); | |||
if (kevent(qfd, &e, 1, NULL, 0, NULL) < 0) { | |||
warn("kevent:"); | |||
return -1; | |||
} | |||
#endif | |||
return 0; | |||
} | |||
int | |||
queue_mod_fd(int qfd, int fd, enum queue_event_type t, const void *data) | |||
{ | |||
#ifdef __linux__ | |||
struct epoll_event e; | |||
/* set event flag (only for non-shared fd's) */ | |||
e.events = EPOLLET; | |||
switch (t) { | |||
case QUEUE_EVENT_IN: | |||
e.events |= EPOLLIN; | |||
break; | |||
case QUEUE_EVENT_OUT: | |||
e.events |= EPOLLOUT; | |||
break; | |||
} | |||
/* set data pointer */ | |||
e.data.ptr = (void *)data; | |||
/* register fd in the interest list */ | |||
if (epoll_ctl(qfd, EPOLL_CTL_MOD, fd, &e) < 0) { | |||
warn("epoll_ctl:"); | |||
return -1; | |||
} | |||
#else | |||
struct kevent e; | |||
int events; | |||
events = EV_CLEAR; | |||
switch (t) { | |||
case QUEUE_EVENT_IN: | |||
events |= EVFILT_READ; | |||
break; | |||
case QUEUE_EVENT_OUT: | |||
events |= EVFILT_WRITE; | |||
break; | |||
} | |||
EV_SET(&e, fd, events, EV_ADD, 0, 0, (void *)data); | |||
if (kevent(qfd, &e, 1, NULL, 0, NULL) < 0) { | |||
warn("kevent:"); | |||
return -1; | |||
} | |||
#endif | |||
return 0; | |||
} | |||
int | |||
queue_rem_fd(int qfd, int fd) | |||
{ | |||
#ifdef __linux__ | |||
struct epoll_event e; | |||
if (epoll_ctl(qfd, EPOLL_CTL_DEL, fd, &e) < 0) { | |||
warn("epoll_ctl:"); | |||
return -1; | |||
} | |||
#else | |||
struct kevent e; | |||
EV_SET(&e, fd, 0, EV_DELETE, 0, 0, 0); | |||
if (kevent(qfd, &e, 1, NULL, 0, NULL) < 0) { | |||
warn("kevent:"); | |||
return -1; | |||
} | |||
#endif | |||
return 0; | |||
} | |||
ssize_t | |||
queue_wait(int qfd, queue_event *e, size_t elen) | |||
{ | |||
ssize_t nready; | |||
#ifdef __linux__ | |||
if ((nready = epoll_wait(qfd, e, elen, -1)) < 0) { | |||
warn("epoll_wait:"); | |||
return -1; | |||
} | |||
#else | |||
if ((nready = kevent(qfd, NULL, 0, e, elen, NULL)) < 0) { | |||
warn("kevent:"); | |||
return -1; | |||
} | |||
#endif | |||
return nready; | |||
} | |||
void * | |||
queue_event_get_data(const queue_event *e) | |||
{ | |||
#ifdef __linux__ | |||
return e->data.ptr; | |||
#else | |||
return e->udata; | |||
#endif | |||
} | |||
int | |||
queue_event_is_error(const queue_event *e) | |||
{ | |||
#ifdef __linux__ | |||
return (e->events & ~(EPOLLIN | EPOLLOUT)) ? 1 : 0; | |||
#else | |||
return (e->flags & EV_EOF) ? 1 : 0; | |||
#endif | |||
} |
@ -0,0 +1,33 @@ | |||
#ifndef QUEUE_H | |||
#define QUEUE_H | |||
#include <stddef.h> | |||
#ifdef __linux__ | |||
#include <sys/epoll.h> | |||
typedef struct epoll_event queue_event; | |||
#else | |||
#include <sys/types.h> | |||
#include <sys/event.h> | |||
#include <sys/time.h> | |||
typedef struct kevent queue_event; | |||
#endif | |||
enum queue_event_type { | |||
QUEUE_EVENT_IN, | |||
QUEUE_EVENT_OUT, | |||
}; | |||
int queue_create(void); | |||
int queue_add_fd(int, int, enum queue_event_type, int, const void *); | |||
int queue_mod_fd(int, int, enum queue_event_type, const void *); | |||
int queue_rem_fd(int, int); | |||
ssize_t queue_wait(int, queue_event *, size_t); | |||
void *queue_event_get_data(const queue_event *); | |||
int queue_event_is_error(const queue_event *e); | |||
#endif /* QUEUE_H */ |
@ -0,0 +1,177 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <errno.h> | |||
#include <pthread.h> | |||
#include <stddef.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include "connection.h" | |||
#include "queue.h" | |||
#include "server.h" | |||
#include "util.h" | |||
struct worker_data { | |||
int insock; | |||
size_t nslots; | |||
const struct server *srv; | |||
}; | |||
static void * | |||
server_worker(void *data) | |||
{ | |||
queue_event *event = NULL; | |||
struct connection *connection, *c, *newc; | |||
struct worker_data *d = (struct worker_data *)data; | |||
int qfd; | |||
ssize_t nready; | |||
size_t i; | |||
/* allocate connections */ | |||
if (!(connection = calloc(d->nslots, sizeof(*connection)))) { | |||
die("calloc:"); | |||
} | |||
/* create event queue */ | |||
if ((qfd = queue_create()) < 0) { | |||
exit(1); | |||
} | |||
/* add insock to the interest list (with data=NULL) */ | |||
if (queue_add_fd(qfd, d->insock, QUEUE_EVENT_IN, 1, NULL) < 0) { | |||
exit(1); | |||
} | |||
/* allocate event array */ | |||
if (!(event = reallocarray(event, d->nslots, sizeof(*event)))) { | |||
die("reallocarray:"); | |||
} | |||
for (;;) { | |||
/* wait for new activity */ | |||
if ((nready = queue_wait(qfd, event, d->nslots)) < 0) { | |||
exit(1); | |||
} | |||
/* handle events */ | |||
for (i = 0; i < (size_t)nready; i++) { | |||
c = queue_event_get_data(&event[i]); | |||
if (queue_event_is_error(&event[i])) { | |||
if (c != NULL) { | |||
queue_rem_fd(qfd, c->fd); | |||
c->res.status = 0; | |||
connection_log(c); | |||
connection_reset(c); | |||
} | |||
continue; | |||
} | |||
if (c == NULL) { | |||
/* add new connection to the interest list */ | |||
if (!(newc = connection_accept(d->insock, | |||
connection, | |||
d->nslots))) { | |||
/* | |||
* the socket is either blocking | |||
* or something failed. | |||
* In both cases, we just carry on | |||
*/ | |||
continue; | |||
} | |||
/* | |||
* add event to the interest list | |||
* (we want IN, because we start | |||
* with receiving the header) | |||
*/ | |||
if (queue_add_fd(qfd, newc->fd, | |||
QUEUE_EVENT_IN, | |||
0, newc) < 0) { | |||
/* not much we can do here */ | |||
continue; | |||
} | |||
} else { | |||
/* serve existing connection */ | |||
connection_serve(c, d->srv); | |||
if (c->fd == 0) { | |||
/* we are done */ | |||
memset(c, 0, sizeof(struct connection)); | |||
continue; | |||
} | |||
/* | |||
* rearm the event based on the state | |||
* we are "stuck" at | |||
*/ | |||
switch(c->state) { | |||
case C_RECV_HEADER: | |||
if (queue_mod_fd(qfd, c->fd, | |||
QUEUE_EVENT_IN, | |||
c) < 0) { | |||
connection_reset(c); | |||
break; | |||
} | |||
break; | |||
case C_SEND_HEADER: | |||
case C_SEND_BODY: | |||
if (queue_mod_fd(qfd, c->fd, | |||
QUEUE_EVENT_OUT, | |||
c) < 0) { | |||
connection_reset(c); | |||
break; | |||
} | |||
break; | |||
default: | |||
break; | |||
} | |||
} | |||
} | |||
} | |||
return NULL; | |||
} | |||
void | |||
server_init_thread_pool(int insock, size_t nthreads, size_t nslots, | |||
const struct server *srv) | |||
{ | |||
pthread_t *thread = NULL; | |||
struct worker_data *d = NULL; | |||
size_t i; | |||
/* allocate worker_data structs */ | |||
if (!(d = reallocarray(d, nthreads, sizeof(*d)))) { | |||
die("reallocarray:"); | |||
} | |||
for (i = 0; i < nthreads; i++) { | |||
d[i].insock = insock; | |||
d[i].nslots = nslots; | |||
d[i].srv = srv; | |||
} | |||
/* allocate and initialize thread pool */ | |||
if (!(thread = reallocarray(thread, nthreads, sizeof(*thread)))) { | |||
die("reallocarray:"); | |||
} | |||
for (i = 0; i < nthreads; i++) { | |||
if (pthread_create(&thread[i], NULL, server_worker, &d[i]) != 0) { | |||
if (errno == EAGAIN) { | |||
die("You need to run as root or have " | |||
"CAP_SYS_RESOURCE set, or are trying " | |||
"to create more threads than the " | |||
"system can offer"); | |||
} else { | |||
die("pthread_create:"); | |||
} | |||
} | |||
} | |||
/* wait for threads */ | |||
for (i = 0; i < nthreads; i++) { | |||
if ((errno = pthread_join(thread[i], NULL))) { | |||
warn("pthread_join:"); | |||
} | |||
} | |||
} |
@ -0,0 +1,35 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef SERVER_H | |||
#define SERVER_H | |||
#include <regex.h> | |||
#include <stddef.h> | |||
struct vhost { | |||
char *chost; | |||
char *regex; | |||
char *dir; | |||
char *prefix; | |||
regex_t re; | |||
}; | |||
struct map { | |||
char *chost; | |||
char *from; | |||
char *to; | |||
}; | |||
struct server { | |||
char *host; | |||
char *port; | |||
char *docindex; | |||
int listdirs; | |||
struct vhost *vhost; | |||
size_t vhost_len; | |||
struct map *map; | |||
size_t map_len; | |||
}; | |||
void server_init_thread_pool(int, size_t, size_t, const struct server *); | |||
#endif /* SERVER_H */ |
@ -0,0 +1,209 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <arpa/inet.h> | |||
#include <errno.h> | |||
#include <fcntl.h> | |||
#include <netdb.h> | |||
#include <netinet/in.h> | |||
#include <stddef.h> | |||
#include <stdio.h> | |||
#include <string.h> | |||
#include <sys/types.h> | |||
#include <sys/socket.h> | |||
#include <sys/stat.h> | |||
#include <sys/time.h> | |||
#include <sys/un.h> | |||
#include <unistd.h> | |||
#include "sock.h" | |||
#include "util.h" | |||
int | |||
sock_get_ips(const char *host, const char* port) | |||
{ | |||
struct addrinfo hints = { | |||
.ai_flags = AI_NUMERICSERV, | |||
.ai_family = AF_UNSPEC, | |||
.ai_socktype = SOCK_STREAM, | |||
}; | |||
struct addrinfo *ai, *p; | |||
int ret, insock = 0; | |||
if ((ret = getaddrinfo(host, port, &hints, &ai))) { | |||
die("getaddrinfo: %s", gai_strerror(ret)); | |||
} | |||
for (p = ai; p; p = p->ai_next) { | |||
if ((insock = socket(p->ai_family, p->ai_socktype, | |||
p->ai_protocol)) < 0) { | |||
continue; | |||
} | |||
if (setsockopt(insock, SOL_SOCKET, SO_REUSEADDR, | |||
&(int){1}, sizeof(int)) < 0) { | |||
die("setsockopt:"); | |||
} | |||
if (bind(insock, p->ai_addr, p->ai_addrlen) < 0) { | |||
/* bind failed, close the insock and retry */ | |||
if (close(insock) < 0) { | |||
die("close:"); | |||
} | |||
continue; | |||
} | |||
break; | |||
} | |||
freeaddrinfo(ai); | |||
if (!p) { | |||
/* we exhaustet the addrinfo-list and found no connection */ | |||
if (errno == EACCES) { | |||
die("You need to run as root or have " | |||
"CAP_NET_BIND_SERVICE set to bind to " | |||
"privileged ports"); | |||
} else { | |||
die("bind:"); | |||
} | |||
} | |||
if (listen(insock, SOMAXCONN) < 0) { | |||
die("listen:"); | |||
} | |||
return insock; | |||
} | |||
int | |||
sock_get_uds(const char *udsname, uid_t uid, gid_t gid) | |||
{ | |||
struct sockaddr_un addr = { | |||
.sun_family = AF_UNIX, | |||
}; | |||
size_t udsnamelen; | |||
int insock, sockmode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | | |||
S_IROTH | S_IWOTH; | |||
if ((insock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { | |||
die("socket:"); | |||
} | |||
if ((udsnamelen = strlen(udsname)) > sizeof(addr.sun_path) - 1) { | |||
die("UNIX-domain socket name truncated"); | |||
} | |||
memcpy(addr.sun_path, udsname, udsnamelen + 1); | |||
if (bind(insock, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { | |||
die("bind '%s':", udsname); | |||
} | |||
if (listen(insock, SOMAXCONN) < 0) { | |||
sock_rem_uds(udsname); | |||
die("listen:"); | |||
} | |||
if (chmod(udsname, sockmode) < 0) { | |||
sock_rem_uds(udsname); | |||
die("chmod '%s':", udsname); | |||
} | |||
if (chown(udsname, uid, gid) < 0) { | |||
sock_rem_uds(udsname); | |||
die("chown '%s':", udsname); | |||
} | |||
return insock; | |||
} | |||
void | |||
sock_rem_uds(const char *udsname) | |||
{ | |||
if (unlink(udsname) < 0) { | |||
die("unlink '%s':", udsname); | |||
} | |||
} | |||
int | |||
sock_set_timeout(int fd, int sec) | |||
{ | |||
struct timeval tv; | |||
tv.tv_sec = sec; | |||
tv.tv_usec = 0; | |||
if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 || | |||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) { | |||
warn("setsockopt:"); | |||
return 1; | |||
} | |||
return 0; | |||
} | |||
int | |||
sock_set_nonblocking(int fd) | |||
{ | |||
int flags; | |||
if ((flags = fcntl(fd, F_GETFL, 0)) < 0) { | |||
warn("fcntl:"); | |||
return 1; | |||
} | |||
flags |= O_NONBLOCK; | |||
if (fcntl(fd, F_SETFL, flags) < 0) { | |||
warn("fcntl:"); | |||
return 1; | |||
} | |||
return 0; | |||
} | |||
int | |||
sock_get_inaddr_str(const struct sockaddr_storage *in_sa, char *str, | |||
size_t len) | |||
{ | |||
switch (in_sa->ss_family) { | |||
case AF_INET: | |||
if (!inet_ntop(AF_INET, | |||
&(((struct sockaddr_in *)in_sa)->sin_addr), | |||
str, len)) { | |||
warn("inet_ntop:"); | |||
return 1; | |||
} | |||
break; | |||
case AF_INET6: | |||
if (!inet_ntop(AF_INET6, | |||
&(((struct sockaddr_in6 *)in_sa)->sin6_addr), | |||
str, len)) { | |||
warn("inet_ntop:"); | |||
return 1; | |||
} | |||
break; | |||
case AF_UNIX: | |||
snprintf(str, len, "uds"); | |||
break; | |||
default: | |||
snprintf(str, len, "-"); | |||
} | |||
return 0; | |||
} | |||
int | |||
sock_same_addr(const struct sockaddr_storage *sa1, const struct sockaddr_storage *sa2) | |||
{ | |||
/* return early if address-families don't match */ | |||
if (sa1->ss_family != sa2->ss_family) { | |||
return 0; | |||
} | |||
switch (sa1->ss_family) { | |||
case AF_INET6: | |||
return memcmp(((struct sockaddr_in6 *)sa1)->sin6_addr.s6_addr, | |||
((struct sockaddr_in6 *)sa2)->sin6_addr.s6_addr, | |||
sizeof(((struct sockaddr_in6 *)sa1)->sin6_addr.s6_addr)); | |||
case AF_INET: | |||
return ntohl(((struct sockaddr_in *)sa1)->sin_addr.s_addr) == | |||
ntohl(((struct sockaddr_in *)sa2)->sin_addr.s_addr); | |||
default: /* AF_UNIX */ | |||
return strcmp(((struct sockaddr_un *)sa1)->sun_path, | |||
((struct sockaddr_un *)sa2)->sun_path) == 0; | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef SOCK_H | |||
#define SOCK_H | |||
#include <stddef.h> | |||
#include <sys/socket.h> | |||
#include <sys/types.h> | |||
int sock_get_ips(const char *, const char *); | |||
int sock_get_uds(const char *, uid_t, gid_t); | |||
void sock_rem_uds(const char *); | |||
int sock_set_timeout(int, int); | |||
int sock_set_nonblocking(int); | |||
int sock_get_inaddr_str(const struct sockaddr_storage *, char *, size_t); | |||
int sock_same_addr(const struct sockaddr_storage *, | |||
const struct sockaddr_storage *); | |||
#endif /* SOCK_H */ |
@ -0,0 +1,281 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#include <errno.h> | |||
#include <limits.h> | |||
#include <stdarg.h> | |||
#include <stdint.h> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <sys/types.h> | |||
#include <time.h> | |||
#ifdef __OpenBSD__ | |||
#include <unistd.h> | |||
#endif /* __OpenBSD__ */ | |||
#include "util.h" | |||
char *argv0; | |||
static void | |||
verr(const char *fmt, va_list ap) | |||
{ | |||
if (argv0 && strncmp(fmt, "usage", sizeof("usage") - 1)) { | |||
fprintf(stderr, "%s: ", argv0); | |||
} | |||
vfprintf(stderr, fmt, ap); | |||
if (fmt[0] && fmt[strlen(fmt) - 1] == ':') { | |||
fputc(' ', stderr); | |||
perror(NULL); | |||
} else { | |||
fputc('\n', stderr); | |||
} | |||
} | |||
void | |||
warn(const char *fmt, ...) | |||
{ | |||
va_list ap; | |||
va_start(ap, fmt); | |||
verr(fmt, ap); | |||
va_end(ap); | |||
} | |||
void | |||
die(const char *fmt, ...) | |||
{ | |||
va_list ap; | |||
va_start(ap, fmt); | |||
verr(fmt, ap); | |||
va_end(ap); | |||
exit(1); | |||
} | |||
void | |||
epledge(const char *promises, const char *execpromises) | |||
{ | |||
(void)promises; | |||
(void)execpromises; | |||
#ifdef __OpenBSD__ | |||
if (pledge(promises, execpromises) == -1) { | |||
die("pledge:"); | |||
} | |||
#endif /* __OpenBSD__ */ | |||
} | |||
void | |||
eunveil(const char *path, const char *permissions) | |||
{ | |||
(void)path; | |||
(void)permissions; | |||
#ifdef __OpenBSD__ | |||
if (unveil(path, permissions) == -1) { | |||
die("unveil:"); | |||
} | |||
#endif /* __OpenBSD__ */ | |||
} | |||
int | |||
timestamp(char *buf, size_t len, time_t t) | |||
{ | |||
struct tm tm; | |||
if (gmtime_r(&t, &tm) == NULL || | |||
strftime(buf, len, "%a, %d %b %Y %T GMT", &tm) == 0) { | |||
return 1; | |||
} | |||
return 0; | |||
} | |||
int | |||
esnprintf(char *str, size_t size, const char *fmt, ...) | |||
{ | |||
va_list ap; | |||
int ret; | |||
va_start(ap, fmt); | |||
ret = vsnprintf(str, size, fmt, ap); | |||
va_end(ap); | |||
return (ret < 0 || (size_t)ret >= size); | |||
} | |||
int | |||
prepend(char *str, size_t size, const char *prefix) | |||
{ | |||
size_t len = strlen(str), prefixlen = strlen(prefix); | |||
if (len + prefixlen + 1 > size) { | |||
return 1; | |||
} | |||
memmove(str + prefixlen, str, len + 1); | |||
memcpy(str, prefix, prefixlen); | |||
return 0; | |||
} | |||
int | |||
spacetok(const char *s, char **t, size_t tlen) | |||
{ | |||
const char *tok; | |||
size_t i, j, toki, spaces; | |||
/* fill token-array with NULL-pointers */ | |||
for (i = 0; i < tlen; i++) { | |||
t[i] = NULL; | |||
} | |||
toki = 0; | |||
/* don't allow NULL string or leading spaces */ | |||
if (!s || *s == ' ') { | |||
return 1; | |||
} | |||
start: | |||
/* skip spaces */ | |||
for (; *s == ' '; s++) | |||
; | |||
/* don't allow trailing spaces */ | |||
if (*s == '\0') { | |||
goto err; | |||
} | |||
/* consume token */ | |||
for (tok = s, spaces = 0; ; s++) { | |||
if (*s == '\\' && *(s + 1) == ' ') { | |||
spaces++; | |||
s++; | |||
continue; | |||
} else if (*s == ' ') { | |||
/* end of token */ | |||
goto token; | |||
} else if (*s == '\0') { | |||
/* end of string */ | |||
goto token; | |||
} | |||
} | |||
token: | |||
if (toki >= tlen) { | |||
goto err; | |||
} | |||
if (!(t[toki] = malloc(s - tok - spaces + 1))) { | |||
die("malloc:"); | |||
} | |||
for (i = 0, j = 0; j < s - tok - spaces + 1; i++, j++) { | |||
if (tok[i] == '\\' && tok[i + 1] == ' ') { | |||
i++; | |||
} | |||
t[toki][j] = tok[i]; | |||
} | |||
t[toki][s - tok - spaces] = '\0'; | |||
toki++; | |||
if (*s == ' ') { | |||
s++; | |||
goto start; | |||
} | |||
return 0; | |||
err: | |||
for (i = 0; i < tlen; i++) { | |||
free(t[i]); | |||
t[i] = NULL; | |||
} | |||
return 1; | |||
} | |||
#define INVALID 1 | |||
#define TOOSMALL 2 | |||
#define TOOLARGE 3 | |||
long long | |||
strtonum(const char *numstr, long long minval, long long maxval, | |||
const char **errstrp) | |||
{ | |||
long long ll = 0; | |||
int error = 0; | |||
char *ep; | |||
struct errval { | |||
const char *errstr; | |||
int err; | |||
} ev[4] = { | |||
{ NULL, 0 }, | |||
{ "invalid", EINVAL }, | |||
{ "too small", ERANGE }, | |||
{ "too large", ERANGE }, | |||
}; | |||
ev[0].err = errno; | |||
errno = 0; | |||
if (minval > maxval) { | |||
error = INVALID; | |||
} else { | |||
ll = strtoll(numstr, &ep, 10); | |||
if (numstr == ep || *ep != '\0') | |||
error = INVALID; | |||
else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) | |||
error = TOOSMALL; | |||
else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) | |||
error = TOOLARGE; | |||
} | |||
if (errstrp != NULL) | |||
*errstrp = ev[error].errstr; | |||
errno = ev[error].err; | |||
if (error) | |||
ll = 0; | |||
return ll; | |||
} | |||
/* | |||
* This is sqrt(SIZE_MAX+1), as s1*s2 <= SIZE_MAX | |||
* if both s1 < MUL_NO_OVERFLOW and s2 < MUL_NO_OVERFLOW | |||
*/ | |||
#define MUL_NO_OVERFLOW ((size_t)1 << (sizeof(size_t) * 4)) | |||
void * | |||
reallocarray(void *optr, size_t nmemb, size_t size) | |||
{ | |||
if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && | |||
nmemb > 0 && SIZE_MAX / nmemb < size) { | |||
errno = ENOMEM; | |||
return NULL; | |||
} | |||
return realloc(optr, size * nmemb); | |||
} | |||
int | |||
buffer_appendf(struct buffer *buf, const char *suffixfmt, ...) | |||
{ | |||
va_list ap; | |||
int ret; | |||
va_start(ap, suffixfmt); | |||
ret = vsnprintf(buf->data + buf->len, | |||
sizeof(buf->data) - buf->len, suffixfmt, ap); | |||
va_end(ap); | |||
if (ret < 0 || (size_t)ret >= (sizeof(buf->data) - buf->len)) { | |||
/* truncation occured, discard and error out */ | |||
memset(buf->data + buf->len, 0, | |||
sizeof(buf->data) - buf->len); | |||
return 1; | |||
} | |||
/* increase buffer length by number of bytes written */ | |||
buf->len += ret; | |||
return 0; | |||
} |
@ -0,0 +1,42 @@ | |||
/* See LICENSE file for copyright and license details. */ | |||
#ifndef UTIL_H | |||
#define UTIL_H | |||
#include <regex.h> | |||
#include <stddef.h> | |||
#include <time.h> | |||
#include "config.h" | |||
/* general purpose buffer */ | |||
struct buffer { | |||
char data[BUFFER_SIZE]; | |||
size_t len; | |||
}; | |||
#undef MIN | |||
#define MIN(x,y) ((x) < (y) ? (x) : (y)) | |||
#undef MAX | |||
#define MAX(x,y) ((x) > (y) ? (x) : (y)) | |||
#undef LEN | |||
#define LEN(x) (sizeof (x) / sizeof *(x)) | |||
extern char *argv0; | |||
void warn(const char *, ...); | |||
void die(const char *, ...); | |||
void epledge(const char *, const char *); | |||
void eunveil(const char *, const char *); | |||
int timestamp(char *, size_t, time_t); | |||
int esnprintf(char *, size_t, const char *, ...); | |||
int prepend(char *, size_t, const char *); | |||
int spacetok(const char *, char **, size_t); | |||
void *reallocarray(void *, size_t, size_t); | |||
long long strtonum(const char *, long long, long long, const char **); | |||
int buffer_appendf(struct buffer *, const char *, ...); | |||
#endif /* UTIL_H */ |