AJ ONeal
4 anos atrás
23 arquivos alterados com 1635 adições e 95 exclusões
@ -0,0 +1 @@ |
|||
package-lock.json |
@ -0,0 +1,16 @@ |
|||
{ "node": true |
|||
, "browser": true |
|||
, "globals": { "Promise": true } |
|||
, "esversion": 8 |
|||
|
|||
, "indent": 2 |
|||
, "onevar": true |
|||
, "laxbreak": true |
|||
, "curly": true |
|||
, "nonbsp": true |
|||
|
|||
, "eqeqeq": true |
|||
, "immed": true |
|||
, "undef": true |
|||
, "unused": true |
|||
} |
@ -0,0 +1 @@ |
|||
dist/ |
@ -0,0 +1,8 @@ |
|||
{ |
|||
"bracketSpacing": true, |
|||
"printWidth": 80, |
|||
"singleQuote": true, |
|||
"tabWidth": 4, |
|||
"trailingComma": "none", |
|||
"useTabs": true |
|||
} |
@ -0,0 +1,79 @@ |
|||
'use strict'; |
|||
/*global crypto*/ |
|||
|
|||
var Hashcash = module.exports; |
|||
|
|||
var textEncoder = new TextEncoder(); |
|||
Hashcash.solve = async function solveHc(hc) { |
|||
var solution = 0; |
|||
var parts = hc.split(':').slice(0, 6); |
|||
if (parts.length < 6) { |
|||
throw new Error('invalid Hashcash-Challenge: ' + hc); |
|||
} |
|||
|
|||
var bits = parseInt(parts[1], 10) || -1; |
|||
if (bits > 10 || bits < 0) { |
|||
throw new Error('bad bit values'); |
|||
} |
|||
console.log('bits:', bits); |
|||
hc = parts.join(':') + ':'; |
|||
async function next() { |
|||
var answer = hc + int52ToBase64(solution); |
|||
var u8 = textEncoder.encode(answer); |
|||
// REALLY SLOW due to async tasks and C++ context switch
|
|||
var hash = await crypto.subtle.digest('SHA-256', u8); |
|||
hash = new Uint8Array(hash); |
|||
if (checkHc(hash, bits)) { |
|||
return answer; |
|||
} |
|||
solution += 1; |
|||
return next(); |
|||
} |
|||
|
|||
return next(); |
|||
}; |
|||
|
|||
function int52ToBase64(n) { |
|||
var hex = n.toString(16); |
|||
if (hex.length % 2) { |
|||
hex = '0' + hex; |
|||
} |
|||
|
|||
var bin = []; |
|||
var i = 0; |
|||
var d; |
|||
var b; |
|||
while (i < hex.length) { |
|||
d = parseInt(hex.slice(i, i + 2), 16); |
|||
b = String.fromCharCode(d); |
|||
bin.push(b); |
|||
i += 2; |
|||
} |
|||
|
|||
return btoa(bin.join('')).replace(/=/g, ''); |
|||
} |
|||
|
|||
function checkHc(hash, bits) { |
|||
var n = Math.floor(bits / 8); |
|||
var m = bits % 8; |
|||
var i; |
|||
if (m > 0) { |
|||
n += 1; |
|||
} |
|||
|
|||
for (i = 0; i < n && i < hash.length; i += 1) { |
|||
if (bits > 8) { |
|||
bits -= 8; |
|||
if (0 !== hash[i]) { |
|||
return false; |
|||
} |
|||
continue; |
|||
} |
|||
|
|||
if (0 !== hash[i] >> (8 - bits)) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
@ -0,0 +1,199 @@ |
|||
'use strict'; |
|||
|
|||
var request = require('./request.js'); |
|||
var PocketId = require('./pocketid.js'); |
|||
var state = {}; |
|||
var auths = clearAuths(); |
|||
|
|||
function $$(sel, el) { |
|||
if (el) { |
|||
return el.querySelectorAll(sel) || []; |
|||
} |
|||
return document.body.querySelectorAll(sel) || []; |
|||
} |
|||
|
|||
function $(sel, el) { |
|||
if (el) { |
|||
return el.querySelector(sel); |
|||
} |
|||
return document.body.querySelector(sel); |
|||
} |
|||
|
|||
function clearAuths() { |
|||
var _auths = { |
|||
google: { |
|||
promise: null, |
|||
idToken: '' |
|||
} |
|||
}; |
|||
_auths.google.promise = new Promise(function (res, rej) { |
|||
_auths.google.resolve = res; |
|||
_auths.google.reject = rej; |
|||
}); |
|||
return _auths; |
|||
} |
|||
|
|||
window.onSignIn = async function onSignIn(googleUser) { |
|||
// Useful data for your client-side scripts:
|
|||
var profile = googleUser.getBasicProfile(); |
|||
// Don't send this directly to your server!
|
|||
console.log('ID: ' + profile.getId()); |
|||
console.log('Full Name: ' + profile.getName()); |
|||
console.log('Given Name: ' + profile.getGivenName()); |
|||
console.log('Family Name: ' + profile.getFamilyName()); |
|||
console.log('Image URL: ' + profile.getImageUrl()); |
|||
console.log('Email: ' + profile.getEmail()); |
|||
|
|||
// The ID token you need to pass to your backend:
|
|||
auths.google.idToken = googleUser.getAuthResponse().id_token; |
|||
console.log('ID Token: ' + auths.google.idToken); |
|||
auths.google.resolve(auths.google.idToken); |
|||
}; |
|||
|
|||
function setFlow(cont, flow) { |
|||
$$(cont).forEach(function (el) { |
|||
el.hidden = true; |
|||
}); |
|||
console.log(flow); |
|||
$(flow).hidden = false; |
|||
} |
|||
|
|||
async function unlock() { |
|||
var key; |
|||
try { |
|||
key = await PocketId.unlock(function () { |
|||
setFlow('.authn-container', '.authn-unlock'); |
|||
return new Promise(function (resolve, reject) { |
|||
window.unlocker = { resolve: resolve, reject: reject }; |
|||
}); |
|||
}); |
|||
} catch (e) { |
|||
console.error( |
|||
"Had a key, but couldn't unlock it. TODO: Just send email?" |
|||
); |
|||
console.error(e); |
|||
return; |
|||
} |
|||
|
|||
setFlow('.authn-container', '.authn-loading'); |
|||
|
|||
if (key) { |
|||
genTokenWithKey(key); |
|||
return; |
|||
await PocketId.createIdToken({ key: key }); |
|||
} |
|||
|
|||
PocketId.signIdToken(id_token).then(function (resp) { |
|||
console.log('Response:'); |
|||
console.log(resp); |
|||
}); |
|||
} |
|||
|
|||
function genTokenWithKey() { |
|||
// TODO: generate token
|
|||
// TODO: check if the key is still considered valid
|
|||
// TODO: generate new key and authorize
|
|||
} |
|||
|
|||
(async function () { |
|||
var loc = window.location; |
|||
|
|||
console.log('/new-hashcash?'); |
|||
var resp = await request({ |
|||
method: 'POST', |
|||
url: loc.protocol + '//' + loc.hostname + '/api/new-hashcash' |
|||
}); |
|||
console.log(resp); |
|||
|
|||
console.log('/test-hashcash?'); |
|||
resp = await request({ |
|||
method: 'POST', |
|||
url: loc.protocol + '//' + loc.hostname + '/api/test-hashcash' |
|||
}); |
|||
console.log(resp); |
|||
})(); |
|||
|
|||
setFlow('.authn-container', '.authn-email'); |
|||
|
|||
$('.authn-email form').addEventListener('submit', function (ev) { |
|||
ev.preventDefault(); |
|||
ev.stopPropagation(); |
|||
state.email = $('.authn-email [name=username]').value; |
|||
|
|||
setFlow('.authn-container', '.authn-loading'); |
|||
return PocketId.auth |
|||
.meta({ email: state.email }) |
|||
.catch(function (err) { |
|||
window.alert('Error: ' + err.message); |
|||
}) |
|||
.then(function (resp) { |
|||
// if the user exists, go to the continue screen
|
|||
// otherwise go to the new user screen
|
|||
console.log('meta:', resp); |
|||
if (!resp.body.success) { |
|||
// This is a completely new user
|
|||
setFlow('.authn-container', '.authn-new-user'); |
|||
return; |
|||
} |
|||
// The user exists, but this is a new device
|
|||
setFlow('.authn-container', '.authn-existing'); |
|||
}); |
|||
}); |
|||
|
|||
$('.authn-new-user form').addEventListener('submit', function (ev) { |
|||
ev.preventDefault(); |
|||
ev.stopPropagation(); |
|||
|
|||
// We don't need to worry about checking if the key exists
|
|||
// even if it does, the account has been deactivated
|
|||
|
|||
setFlow('.authn-container', '.authn-loading'); |
|||
return PocketId.auth |
|||
.verify({ scheme: 'mailto:', email: state.email }) |
|||
.catch(function (err) { |
|||
window.alert('Error: ' + err.message); |
|||
}) |
|||
.then(function (resp) { |
|||
console.log(resp); |
|||
localStorage.setItem( |
|||
'pocketid', // + state.email,
|
|||
JSON.stringify({ |
|||
receipt: resp.body.receipt, |
|||
email: state.email, |
|||
createdAt: new Date().toISOString() |
|||
}) |
|||
); |
|||
window.alert("Go check yo' email!"); |
|||
return PocketId.auth |
|||
.consume({ |
|||
email: state.email, |
|||
receipt: resp.body.receipt |
|||
}) |
|||
.then(function (resp) { |
|||
window.alert('all set!'); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
var route = window.location.hash.split('/').slice(1); |
|||
console.log('route:', route); |
|||
switch (route[0]) { |
|||
case 'verify': |
|||
var pstate = JSON.parse(localStorage.getItem('pocketid') || '{}'); |
|||
PocketId.auth |
|||
.consume({ |
|||
receipt: pstate.receipt, |
|||
secret: route[1] |
|||
}) |
|||
.then(function (resp) { |
|||
console.log('token for this device to save:', resp); |
|||
window.alert('goodness!'); |
|||
}) |
|||
.catch(function (e) { |
|||
console.error(e); |
|||
window.alert('network error, try again'); |
|||
}); |
|||
break; |
|||
default: |
|||
// do nothing
|
|||
} |
@ -0,0 +1,34 @@ |
|||
{ |
|||
"name": "pocketid", |
|||
"version": "0.1.0", |
|||
"description": "ID tokens made easy", |
|||
"main": "pocketid.js", |
|||
"scripts": { |
|||
"prettier": "prettier --write '**/*.{css,js,md}'", |
|||
"test": "node pocketid_test.js" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://example.com/pocketid.git" |
|||
}, |
|||
"keywords": [ |
|||
"oauth1", |
|||
"oauth2", |
|||
"oauth3", |
|||
"oidc", |
|||
"acme", |
|||
"jwt", |
|||
"jose", |
|||
"jws", |
|||
"jwk" |
|||
], |
|||
"author": "AJ ONeal <coolaj86@gmail.com>", |
|||
"license": "MPL-2.0", |
|||
"dependencies": { |
|||
"@root/keypairs": "^0.10.1" |
|||
}, |
|||
"devDependencies": { |
|||
"webpack": "^5.0.0-beta.28", |
|||
"webpack-cli": "^3.3.12" |
|||
} |
|||
} |
@ -0,0 +1,118 @@ |
|||
'use strict'; |
|||
|
|||
var Keypairs = require('@root/keypairs'); |
|||
var PocketId = module.exports; |
|||
var request = require('./request.js'); |
|||
|
|||
var keyJson = window.localStorage.getItem('private.jwk.json'); |
|||
|
|||
PocketId.signIdToken = async function (idToken) { |
|||
var pair = await Keypairs.parseOrGenerate({ key: keyJson }); |
|||
var jwt = await Keypairs.signJwt({ |
|||
jwk: pair.private, |
|||
iss: window.location.protocol + '//' + window.location.hostname, |
|||
exp: '15m', |
|||
claims: { |
|||
contact: ['google:' + idToken] |
|||
} |
|||
}); |
|||
return jwt; |
|||
}; |
|||
|
|||
PocketId.auth = {}; |
|||
PocketId.auth.meta = async function ({ email }) { |
|||
var loc = window.location; |
|||
var body = await request({ |
|||
method: 'GET', |
|||
url: |
|||
loc.protocol + |
|||
'//' + |
|||
loc.hostname + |
|||
'/api/authn/meta?contact=' + |
|||
'mailto:' + |
|||
email |
|||
}); |
|||
return body; |
|||
}; |
|||
|
|||
PocketId.auth.verify = async function ({ scheme, email }) { |
|||
if (!scheme) { |
|||
scheme = 'mailto:'; |
|||
} |
|||
|
|||
var loc = window.location; |
|||
var body = await request({ |
|||
method: 'GET', |
|||
url: |
|||
loc.protocol + |
|||
'//' + |
|||
loc.hostname + |
|||
'/api/authn/verify?contact=' + |
|||
scheme + |
|||
email |
|||
}); |
|||
|
|||
return body; |
|||
}; |
|||
|
|||
PocketId.auth.consume = async function ({ |
|||
email = '', |
|||
receipt = '', |
|||
secret = '', |
|||
count = 0 |
|||
}) { |
|||
var loc = window.location; |
|||
var resp = await request({ |
|||
method: 'GET', |
|||
url: |
|||
loc.protocol + |
|||
'//' + |
|||
loc.hostname + |
|||
'/api/authn/consume?contact=' + |
|||
(email ? 'mailto:' + email : '') + |
|||
'&receipt=' + |
|||
receipt + |
|||
'&secret=' + |
|||
secret |
|||
}); |
|||
|
|||
if (resp.body.success) { |
|||
// There should be a token here
|
|||
// (or the pubkey should have been given beforehand)
|
|||
return resp.body; |
|||
} |
|||
|
|||
if (resp.body.error) { |
|||
// TODO special errors are hard failures
|
|||
} |
|||
|
|||
if (count > 600) { |
|||
throw new Error('abandoned login'); |
|||
} |
|||
|
|||
return timeout(5000).then(function () { |
|||
console.log('check otp again'); |
|||
return PocketId.auth.consume({ |
|||
email, |
|||
secret, |
|||
receipt, |
|||
count: count || 0 |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
async function timeout(ms) { |
|||
return new Promise(function (resolve) { |
|||
setTimeout(resolve, ms); |
|||
}); |
|||
} |
|||
|
|||
var textEncoder = new TextEncoder(); |
|||
PocketId.genKey = async function ({ email }) { |
|||
// Ideally we'd use PBKDF2 or better but... web standards...
|
|||
// TODO put a random salt
|
|||
var emailU8 = textEncoder.encode(email); |
|||
var salt = await crypto.subtle.digest('SHA-256', emailU8); |
|||
var u8 = textEncoder.encode(answer); |
|||
var hash = await crypto.subtle.digest('SHA-256', u8); |
|||
}; |
@ -0,0 +1,85 @@ |
|||
'use strict'; |
|||
|
|||
var Hashcash = require('./hashcash.js'); |
|||
var _sites = {}; |
|||
|
|||
module.exports = async function (opts) { |
|||
if (!opts.headers) { |
|||
opts.headers = {}; |
|||
} |
|||
|
|||
if (opts.json) { |
|||
if (true === opts.json) { |
|||
opts.body = JSON.stringify(opts.body); |
|||
} else { |
|||
opts.body = JSON.stringify(opts.json); |
|||
} |
|||
if (!opts.headers['Content-Type'] && !opts.headers['content-type']) { |
|||
opts.headers['Content-Type'] = 'application/json'; |
|||
} |
|||
} |
|||
|
|||
if (!opts.mode) { |
|||
opts.mode = 'cors'; |
|||
} |
|||
|
|||
var url = new URL(opts.url); |
|||
if (!_sites[url.hostname]) { |
|||
_sites[url.hostname] = { nonces: [] }; |
|||
} |
|||
|
|||
var site = _sites[url.hostname]; |
|||
var hc = site.hashcashChallenge; |
|||
if (hc) { |
|||
delete site.hashcashChallenge; |
|||
site.hashcash = await Hashcash.solve(hc); |
|||
} |
|||
if (site.hashcash) { |
|||
opts.headers.Hashcash = site.hashcash; |
|||
} |
|||
|
|||
var response = await window.fetch(opts.url, opts); |
|||
var headerNames = response.headers.keys(); |
|||
var hs = {}; |
|||
var h; |
|||
while (true) { |
|||
h = headerNames.next(); |
|||
if (h.done) { |
|||
break; |
|||
} |
|||
hs[h.value] = response.headers.get(h.value); |
|||
} |
|||
|
|||
var body; |
|||
if (hs['content-type'].includes('application/json')) { |
|||
body = await response.json(); |
|||
} else { |
|||
body = await response.text(); |
|||
try { |
|||
body = JSON.parse(body); |
|||
} catch (e) { |
|||
// ignore
|
|||
} |
|||
} |
|||
var resp = {}; |
|||
|
|||
resp.body = body; |
|||
resp.headers = hs; |
|||
resp.toJSON = function () { |
|||
return { |
|||
headers: hs, |
|||
body: body |
|||
}; |
|||
}; |
|||
|
|||
if (resp.headers['hashcash-challenge']) { |
|||
_sites[url.hostname].hashcashChallenge = |
|||
resp.headers['hashcash-challenge']; |
|||
} |
|||
if (resp.headers.nonce) { |
|||
site.nonces.push(resp.headers.nonce); |
|||
} |
|||
|
|||
console.log(resp); |
|||
return resp; |
|||
}; |
@ -0,0 +1,19 @@ |
|||
'use strict'; |
|||
|
|||
var path = require('path'); |
|||
|
|||
module.exports = { |
|||
entry: './main.js', |
|||
mode: 'development', |
|||
devServer: { |
|||
contentBase: path.join(__dirname, 'dist'), |
|||
port: 3001 |
|||
}, |
|||
output: { |
|||
publicPath: 'http://localhost:3001/' |
|||
}, |
|||
module: { |
|||
rules: [{}] |
|||
}, |
|||
plugins: [] |
|||
}; |
@ -0,0 +1,4 @@ |
|||
old.go |
|||
old_test.go |
|||
file.txt |
|||
user_agents.txt |
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2017 Miloš Mileusnić |
|||
|
|||
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,97 @@ |
|||
# Go/Golang package for parsing user agent strings [![GoDoc](https://godoc.org/github.com/mileusna/useragent?status.svg)](https://godoc.org/github.com/mileusna/useragent) |
|||
|
|||
Package `ua.Parse(userAgent string)` function parses browser's and bot's user agents strings and determins: |
|||
+ User agent name and version (Chrome, Firefox, Googlebot, etc.) |
|||
+ Operating system name and version (Windows, Android, iOS etc.) |
|||
+ Device type (mobile, desktop, tablet, bot) |
|||
+ Device name if available (iPhone, iPad, Huawei VNS-L21) |
|||
+ URL provided by the bot (http://www.google.com/bot.html etc.) |
|||
|
|||
## Status |
|||
|
|||
Still need some work on detecting Andorid device names. |
|||
|
|||
## Installation <a id="installation"></a> |
|||
``` |
|||
go get github.com/mileusna/useragent |
|||
``` |
|||
|
|||
## Example<a id="example"></a> |
|||
|
|||
```go |
|||
package main |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strings" |
|||
|
|||
"github.com/mileusna/useragent" |
|||
) |
|||
|
|||
func main() { |
|||
userAgents := []string{ |
|||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8", |
|||
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36", |
|||
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", |
|||
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) FxiOS/8.1.1b4948 Mobile/14F89 Safari/603.2.4", |
|||
"Mozilla/5.0 (iPad; CPU OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1", |
|||
"Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36", |
|||
"Mozilla/5.0 (Android 4.3; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0", |
|||
"Mozilla/5.0 (Linux; Android 4.3; GT-I9300 Build/JSS15J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36 OPR/42.9.2246.119956", |
|||
"Opera/9.80 (Android; Opera Mini/28.0.2254/66.318; U; en) Presto/2.12.423 Version/12.16", |
|||
} |
|||
|
|||
for _, s := range userAgents { |
|||
ua := ua.Parse(s) |
|||
fmt.Println() |
|||
fmt.Println(ua.String) |
|||
fmt.Println(strings.Repeat("=", len(ua.String))) |
|||
fmt.Println("Name:", ua.Name, "v", ua.Version) |
|||
fmt.Println("OS:", ua.OS, "v", ua.OSVersion) |
|||
fmt.Println("Device:", ua.Device) |
|||
if ua.Mobile { |
|||
fmt.Println("(Mobile)") |
|||
} |
|||
if ua.Tablet { |
|||
fmt.Println("(Tablet)") |
|||
} |
|||
if ua.Desktop { |
|||
fmt.Println("(Desktop)") |
|||
} |
|||
if ua.Bot { |
|||
fmt.Println("(Bot)") |
|||
} |
|||
if ua.URL != "" { |
|||
fmt.Println(ua.URL) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
``` |
|||
|
|||
## Shorthand functions |
|||
|
|||
Beside `UserAgent{}` struct and its properties returned by `ua.Parse()`, there is a bunch of shorthand functions for most popular browsers and operating systems, so this code: |
|||
|
|||
```go |
|||
ua := ua.Parse(userAgentString) |
|||
if ua.OS == "Android" && ua.Name == "Chrome" { |
|||
// do something |
|||
} |
|||
``` |
|||
can be also written on this way: |
|||
```go |
|||
ua := ua.Parse(userAgentString) |
|||
if ua.IsAndroid() && ua.IsChrome() { |
|||
// do something |
|||
} |
|||
``` |
|||
|
|||
## Notice |
|||
|
|||
+ Opera and Opera Mini are two browsers, since they operate on very different ways. |
|||
+ If Googlebot (or any other bot) is detected and it is using its mobile crawler, both `bot` and `mobile` flags will be set to `true`. |
|||
|
|||
|
|||
|
@ -0,0 +1,3 @@ |
|||
module github.com/mileusna/useragent |
|||
|
|||
go 1.14 |
@ -0,0 +1,76 @@ |
|||
package ua |
|||
|
|||
// IsWindows shorthand function to check if OS == Windows
|
|||
func (ua UserAgent) IsWindows() bool { |
|||
return ua.OS == Windows |
|||
} |
|||
|
|||
// IsAndroid shorthand function to check if OS == Android
|
|||
func (ua UserAgent) IsAndroid() bool { |
|||
return ua.OS == Android |
|||
} |
|||
|
|||
// IsMacOS shorthand function to check if OS == MacOS
|
|||
func (ua UserAgent) IsMacOS() bool { |
|||
return ua.OS == MacOS |
|||
} |
|||
|
|||
// IsIOS shorthand function to check if OS == IOS
|
|||
func (ua UserAgent) IsIOS() bool { |
|||
return ua.OS == IOS |
|||
} |
|||
|
|||
// IsLinux shorthand function to check if OS == Linux
|
|||
func (ua UserAgent) IsLinux() bool { |
|||
return ua.OS == Linux |
|||
} |
|||
|
|||
// IsOpera shorthand function to check if Name == Opera
|
|||
func (ua UserAgent) IsOpera() bool { |
|||
return ua.Name == Opera |
|||
} |
|||
|
|||
// IsOperaMini shorthand function to check if Name == Opera Mini
|
|||
func (ua UserAgent) IsOperaMini() bool { |
|||
return ua.Name == OperaMini |
|||
} |
|||
|
|||
// IsChrome shorthand function to check if Name == Chrome
|
|||
func (ua UserAgent) IsChrome() bool { |
|||
return ua.Name == Chrome |
|||
} |
|||
|
|||
// IsFirefox shorthand function to check if Name == Firefox
|
|||
func (ua UserAgent) IsFirefox() bool { |
|||
return ua.Name == Firefox |
|||
} |
|||
|
|||
// IsInternetExplorer shorthand function to check if Name == Internet Explorer
|
|||
func (ua UserAgent) IsInternetExplorer() bool { |
|||
return ua.Name == InternetExplorer |
|||
} |
|||
|
|||
// IsSafari shorthand function to check if Name == Safari
|
|||
func (ua UserAgent) IsSafari() bool { |
|||
return ua.Name == Safari |
|||
} |
|||
|
|||
// IsEdge shorthand function to check if Name == Edge
|
|||
func (ua UserAgent) IsEdge() bool { |
|||
return ua.Name == Edge |
|||
} |
|||
|
|||
// IsGooglebot shorthand function to check if Name == Googlebot
|
|||
func (ua UserAgent) IsGooglebot() bool { |
|||
return ua.Name == Googlebot |
|||
} |
|||
|
|||
// IsTwitterbot shorthand function to check if Name == Twitterbot
|
|||
func (ua UserAgent) IsTwitterbot() bool { |
|||
return ua.Name == Twitterbot |
|||
} |
|||
|
|||
// IsFacebookbot shorthand function to check if Name == FacebookExternalHit
|
|||
func (ua UserAgent) IsFacebookbot() bool { |
|||
return ua.Name == FacebookExternalHit |
|||
} |
@ -0,0 +1,434 @@ |
|||
package ua |
|||
|
|||
import ( |
|||
"bytes" |
|||
"regexp" |
|||
"strings" |
|||
) |
|||
|
|||
// UserAgent struct containg all determined datra from parsed user-agent string
|
|||
type UserAgent struct { |
|||
Name string |
|||
Version string |
|||
OS string |
|||
OSVersion string |
|||
Device string |
|||
Mobile bool |
|||
Tablet bool |
|||
Desktop bool |
|||
Bot bool |
|||
URL string |
|||
String string |
|||
} |
|||
|
|||
var ignore = map[string]struct{}{ |
|||
"KHTML, like Gecko": struct{}{}, |
|||
"U": struct{}{}, |
|||
"compatible": struct{}{}, |
|||
"Mozilla": struct{}{}, |
|||
"WOW64": struct{}{}, |
|||
} |
|||
|
|||
// Constants for browsers and operating systems for easier comparation
|
|||
const ( |
|||
Windows = "Windows" |
|||
WindowsPhone = "Windows Phone" |
|||
Android = "Android" |
|||
MacOS = "macOS" |
|||
IOS = "iOS" |
|||
Linux = "Linux" |
|||
|
|||
Opera = "Opera" |
|||
OperaMini = "Opera Mini" |
|||
OperaTouch = "Opera Touch" |
|||
Chrome = "Chrome" |
|||
Firefox = "Firefox" |
|||
InternetExplorer = "Internet Explorer" |
|||
Safari = "Safari" |
|||
Edge = "Edge" |
|||
Vivaldi = "Vivaldi" |
|||
|
|||
Googlebot = "Googlebot" |
|||
Twitterbot = "Twitterbot" |
|||
FacebookExternalHit = "facebookexternalhit" |
|||
Applebot = "Applebot" |
|||
) |
|||
|
|||
// Parse user agent string returning UserAgent struct
|
|||
func Parse(userAgent string) UserAgent { |
|||
ua := UserAgent{ |
|||
String: userAgent, |
|||
} |
|||
|
|||
tokens := parse(userAgent) |
|||
|
|||
// check is there URL
|
|||
for k := range tokens { |
|||
if strings.HasPrefix(k, "http://") || strings.HasPrefix(k, "https://") { |
|||
ua.URL = k |
|||
delete(tokens, k) |
|||
break |
|||
} |
|||
} |
|||
|
|||
// OS lookup
|
|||
switch { |
|||
case tokens.exists("Android"): |
|||
ua.OS = Android |
|||
ua.OSVersion = tokens[Android] |
|||
for s := range tokens { |
|||
if strings.HasSuffix(s, "Build") { |
|||
ua.Device = strings.TrimSpace(s[:len(s)-5]) |
|||
ua.Tablet = strings.Contains(strings.ToLower(ua.Device), "tablet") |
|||
} |
|||
} |
|||
|
|||
case tokens.exists("iPhone"): |
|||
ua.OS = IOS |
|||
ua.OSVersion = tokens.findMacOSVersion() |
|||
ua.Device = "iPhone" |
|||
ua.Mobile = true |
|||
|
|||
case tokens.exists("iPad"): |
|||
ua.OS = IOS |
|||
ua.OSVersion = tokens.findMacOSVersion() |
|||
ua.Device = "iPad" |
|||
ua.Tablet = true |
|||
|
|||
case tokens.exists("Windows NT"): |
|||
ua.OS = Windows |
|||
ua.OSVersion = tokens["Windows NT"] |
|||
ua.Desktop = true |
|||
|
|||
case tokens.exists("Windows Phone OS"): |
|||
ua.OS = WindowsPhone |
|||
ua.OSVersion = tokens["Windows Phone OS"] |
|||
ua.Mobile = true |
|||
|
|||
case tokens.exists("Macintosh"): |
|||
ua.OS = MacOS |
|||
ua.OSVersion = tokens.findMacOSVersion() |
|||
ua.Desktop = true |
|||
|
|||
case tokens.exists("Linux"): |
|||
ua.OS = Linux |
|||
ua.OSVersion = tokens[Linux] |
|||
ua.Desktop = true |
|||
|
|||
} |
|||
|
|||
// for s, val := range sys {
|
|||
// fmt.Println(s, "--", val)
|
|||
// }
|
|||
|
|||
switch { |
|||
|
|||
case tokens.exists("Googlebot"): |
|||
ua.Name = Googlebot |
|||
ua.Version = tokens[Googlebot] |
|||
ua.Bot = true |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens.exists("Applebot"): |
|||
ua.Name = Applebot |
|||
ua.Version = tokens[Applebot] |
|||
ua.Bot = true |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
ua.OS = "" |
|||
|
|||
case tokens["Opera Mini"] != "": |
|||
ua.Name = OperaMini |
|||
ua.Version = tokens[OperaMini] |
|||
ua.Mobile = true |
|||
|
|||
case tokens["OPR"] != "": |
|||
ua.Name = Opera |
|||
ua.Version = tokens["OPR"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["OPT"] != "": |
|||
ua.Name = OperaTouch |
|||
ua.Version = tokens["OPT"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
// Opera on iOS
|
|||
case tokens["OPiOS"] != "": |
|||
ua.Name = Opera |
|||
ua.Version = tokens["OPiOS"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
// Chrome on iOS
|
|||
case tokens["CriOS"] != "": |
|||
ua.Name = Chrome |
|||
ua.Version = tokens["CriOS"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
// Firefox on iOS
|
|||
case tokens["FxiOS"] != "": |
|||
ua.Name = Firefox |
|||
ua.Version = tokens["FxiOS"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["Firefox"] != "": |
|||
ua.Name = Firefox |
|||
ua.Version = tokens[Firefox] |
|||
_, ua.Mobile = tokens["Mobile"] |
|||
_, ua.Tablet = tokens["Tablet"] |
|||
|
|||
case tokens["Vivaldi"] != "": |
|||
ua.Name = Vivaldi |
|||
ua.Version = tokens[Vivaldi] |
|||
|
|||
case tokens.exists("MSIE"): |
|||
ua.Name = InternetExplorer |
|||
ua.Version = tokens["MSIE"] |
|||
|
|||
case tokens["EdgiOS"] != "": |
|||
ua.Name = Edge |
|||
ua.Version = tokens["EdgiOS"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["Edge"] != "": |
|||
ua.Name = Edge |
|||
ua.Version = tokens["Edge"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["Edg"] != "": |
|||
ua.Name = Edge |
|||
ua.Version = tokens["Edg"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["EdgA"] != "": |
|||
ua.Name = Edge |
|||
ua.Version = tokens["EdgA"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["bingbot"] != "": |
|||
ua.Name = "Bingbot" |
|||
ua.Version = tokens["bingbot"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens["SamsungBrowser"] != "": |
|||
ua.Name = "Samsung Browser" |
|||
ua.Version = tokens["SamsungBrowser"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
// if chrome and Safari defined, find any other tokensent descr
|
|||
case tokens.exists(Chrome) && tokens.exists(Safari): |
|||
name := tokens.findBestMatch(true) |
|||
if name != "" { |
|||
ua.Name = name |
|||
ua.Version = tokens[name] |
|||
break |
|||
} |
|||
fallthrough |
|||
|
|||
case tokens.exists("Chrome"): |
|||
ua.Name = Chrome |
|||
ua.Version = tokens["Chrome"] |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
case tokens.exists("Safari"): |
|||
ua.Name = Safari |
|||
if v, ok := tokens["Version"]; ok { |
|||
ua.Version = v |
|||
} else { |
|||
ua.Version = tokens["Safari"] |
|||
} |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
|
|||
default: |
|||
if ua.OS == "Android" && tokens["Version"] != "" { |
|||
ua.Name = "Android browser" |
|||
ua.Version = tokens["Version"] |
|||
ua.Mobile = true |
|||
} else { |
|||
if name := tokens.findBestMatch(false); name != "" { |
|||
ua.Name = name |
|||
ua.Version = tokens[name] |
|||
} else { |
|||
ua.Name = ua.String |
|||
} |
|||
ua.Bot = strings.Contains(strings.ToLower(ua.Name), "bot") |
|||
ua.Mobile = tokens.existsAny("Mobile", "Mobile Safari") |
|||
} |
|||
} |
|||
|
|||
// if tabler, switch mobile to off
|
|||
if ua.Tablet { |
|||
ua.Mobile = false |
|||
} |
|||
|
|||
// if not already bot, check some popular bots and weather URL is set
|
|||
if !ua.Bot { |
|||
ua.Bot = ua.URL != "" |
|||
} |
|||
|
|||
if !ua.Bot { |
|||
switch ua.Name { |
|||
case Twitterbot, FacebookExternalHit: |
|||
ua.Bot = true |
|||
} |
|||
} |
|||
|
|||
return ua |
|||
} |
|||
|
|||
func parse(userAgent string) (clients properties) { |
|||
clients = make(map[string]string, 0) |
|||
slash := false |
|||
isURL := false |
|||
var buff, val bytes.Buffer |
|||
addToken := func() { |
|||
if buff.Len() != 0 { |
|||
s := strings.TrimSpace(buff.String()) |
|||
if _, ign := ignore[s]; !ign { |
|||
if isURL { |
|||
s = strings.TrimPrefix(s, "+") |
|||
} |
|||
|
|||
if val.Len() == 0 { // only if value don't exists
|
|||
var ver string |
|||
s, ver = checkVer(s) // determin version string and split
|
|||
clients[s] = ver |
|||
} else { |
|||
clients[s] = strings.TrimSpace(val.String()) |
|||
} |
|||
} |
|||
} |
|||
buff.Reset() |
|||
val.Reset() |
|||
slash = false |
|||
isURL = false |
|||
} |
|||
|
|||
parOpen := false |
|||
|
|||
bua := []byte(userAgent) |
|||
for i, c := range bua { |
|||
|
|||
//fmt.Println(string(c), c)
|
|||
switch { |
|||
case c == 41: // )
|
|||
addToken() |
|||
parOpen = false |
|||
|
|||
case parOpen && c == 59: // ;
|
|||
addToken() |
|||
|
|||
case c == 40: // (
|
|||
addToken() |
|||
parOpen = true |
|||
|
|||
case slash && c == 32: |
|||
addToken() |
|||
|
|||
case slash: |
|||
val.WriteByte(c) |
|||
|
|||
case c == 47 && !isURL: // /
|
|||
if i != len(bua)-1 && bua[i+1] == 47 && (bytes.HasSuffix(buff.Bytes(), []byte("http:")) || bytes.HasSuffix(buff.Bytes(), []byte("https:"))) { |
|||
buff.WriteByte(c) |
|||
isURL = true |
|||
} else { |
|||
slash = true |
|||
} |
|||
|
|||
default: |
|||
buff.WriteByte(c) |
|||
} |
|||
} |
|||
addToken() |
|||
|
|||
return clients |
|||
} |
|||
|
|||
func checkVer(s string) (name, v string) { |
|||
i := strings.LastIndex(s, " ") |
|||
if i == -1 { |
|||
return s, "" |
|||
} |
|||
|
|||
//v = s[i+1:]
|
|||
|
|||
switch s[:i] { |
|||
case "Linux", "Windows NT", "Windows Phone OS", "MSIE", "Android": |
|||
return s[:i], s[i+1:] |
|||
default: |
|||
return s, "" |
|||
} |
|||
|
|||
// for _, c := range v {
|
|||
// if (c >= 48 && c <= 57) || c == 46 {
|
|||
// } else {
|
|||
// return s, ""
|
|||
// }
|
|||
// }
|
|||
|
|||
// return s[:i], s[i+1:]
|
|||
|
|||
} |
|||
|
|||
type properties map[string]string |
|||
|
|||
func (p properties) exists(key string) bool { |
|||
_, ok := p[key] |
|||
return ok |
|||
} |
|||
|
|||
func (p properties) existsAny(keys ...string) bool { |
|||
for _, k := range keys { |
|||
if _, ok := p[k]; ok { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
func (p properties) findMacOSVersion() string { |
|||
for k, v := range p { |
|||
if strings.Contains(k, "OS") { |
|||
if ver := findVersion(v); ver != "" { |
|||
return ver |
|||
} else if ver = findVersion(k); ver != "" { |
|||
return ver |
|||
} |
|||
} |
|||
} |
|||
return "" |
|||
} |
|||
|
|||
// findBestMatch from the rest of the bunch
|
|||
// in first cycle only return key vith version value
|
|||
// if withVerValue is false, do another cycle and return any token
|
|||
func (p properties) findBestMatch(withVerOnly bool) string { |
|||
n := 2 |
|||
if withVerOnly { |
|||
n = 1 |
|||
} |
|||
for i := 0; i < n; i++ { |
|||
for k, v := range p { |
|||
switch k { |
|||
case Chrome, Firefox, Safari, "Version", "Mobile", "Mobile Safari", "Mozilla", "AppleWebKit", "Windows NT", "Windows Phone OS", Android, "Macintosh", Linux, "GSA": |
|||
default: |
|||
if i == 0 { |
|||
if v != "" { // in first check, only return keys with value
|
|||
return k |
|||
} |
|||
} else { |
|||
return k |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return "" |
|||
} |
|||
|
|||
var rxMacOSVer = regexp.MustCompile("[_\\d\\.]+") |
|||
|
|||
func findVersion(s string) string { |
|||
if ver := rxMacOSVer.FindString(s); ver != "" { |
|||
return strings.Replace(ver, "_", ".", -1) |
|||
} |
|||
return "" |
|||
} |
Carregando…
Reference in new issue