Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04ffde105 | ||
|
|
ab24ab2f1a | ||
|
|
8323c0c4b6 | ||
|
|
b28582d630 | ||
|
|
c047053646 | ||
|
|
41649418fc | ||
|
|
3b443bc9c8 | ||
|
|
03c4793010 | ||
|
|
9d29fdea4b | ||
|
|
49e535336b | ||
|
|
2a2647dc3f | ||
|
|
0ede2b13c9 | ||
|
|
c185f6826a | ||
|
|
e8bb0fdcb7 | ||
|
|
0737354f53 | ||
|
|
c74ae0d0e7 | ||
|
|
15e6311c63 | ||
|
|
6ba1d857ac | ||
|
|
67f31ccf43 | ||
|
|
a52c4b4362 | ||
|
|
235b198ef9 | ||
|
|
ddfd53bf06 | ||
|
|
ffffd74a6e | ||
|
|
b870db249e | ||
|
|
76c9e61199 | ||
|
|
0579e9bf99 | ||
|
|
c70389eb30 | ||
|
|
5bcd1545a8 | ||
|
|
8985faa95d | ||
|
|
2dc31bee20 | ||
|
|
3269766ea7 | ||
|
|
b7b32e2f01 | ||
|
|
bd1ee48a4f |
@@ -54,6 +54,7 @@ Contents:
|
||||
* Log-in page
|
||||
* API: Log in
|
||||
* API: Log out
|
||||
* API: Get current user info
|
||||
|
||||
|
||||
## Relations between subsystems
|
||||
@@ -1207,7 +1208,7 @@ YAML configuration:
|
||||
|
||||
Session DB file:
|
||||
|
||||
session="..." expire=123456
|
||||
session="..." user=name expire=123456
|
||||
...
|
||||
|
||||
Session data is SHA(random()+name+password).
|
||||
@@ -1270,3 +1271,20 @@ Response:
|
||||
302 Found
|
||||
Location: /login.html
|
||||
Set-Cookie: session=...; Expires=Thu, 01 Jan 1970 00:00:00 GMT
|
||||
|
||||
|
||||
### API: Get current user info
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/profile
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"name":"..."
|
||||
}
|
||||
|
||||
If no client is configured then authentication is disabled and server sends an empty response.
|
||||
|
||||
@@ -213,6 +213,21 @@ export const getClients = () => async (dispatch) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getProfileRequest = createAction('GET_PROFILE_REQUEST');
|
||||
export const getProfileFailure = createAction('GET_PROFILE_FAILURE');
|
||||
export const getProfileSuccess = createAction('GET_PROFILE_SUCCESS');
|
||||
|
||||
export const getProfile = () => async (dispatch) => {
|
||||
dispatch(getProfileRequest());
|
||||
try {
|
||||
const profile = await apiClient.getProfile();
|
||||
dispatch(getProfileSuccess(profile));
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(getProfileFailure());
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
|
||||
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
|
||||
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
|
||||
@@ -224,6 +239,7 @@ export const getDnsStatus = () => async (dispatch) => {
|
||||
dispatch(dnsStatusSuccess(dnsStatus));
|
||||
dispatch(getVersion());
|
||||
dispatch(getTlsStatus());
|
||||
dispatch(getProfile());
|
||||
} catch (error) {
|
||||
dispatch(addErrorToast({ error }));
|
||||
dispatch(dnsStatusFailure());
|
||||
|
||||
@@ -525,6 +525,14 @@ class Api {
|
||||
};
|
||||
return this.makeRequest(path, method, config);
|
||||
}
|
||||
|
||||
// Profile
|
||||
GET_PROFILE = { path: 'profile', method: 'GET' };
|
||||
|
||||
getProfile() {
|
||||
const { path, method } = this.GET_PROFILE;
|
||||
return this.makeRequest(path, method);
|
||||
}
|
||||
}
|
||||
|
||||
const apiClient = new Api();
|
||||
|
||||
@@ -60,9 +60,11 @@ class Header extends Component {
|
||||
/>
|
||||
<div className="header__column">
|
||||
<div className="header__right">
|
||||
<a href="/control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
<Trans>sign_out</Trans>
|
||||
</a>
|
||||
{!dashboard.processingProfile && dashboard.name &&
|
||||
<a href="/control/logout" className="btn btn-sm btn-outline-secondary">
|
||||
<Trans>sign_out</Trans>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,10 +30,6 @@ export const REPOSITORY = {
|
||||
export const PRIVACY_POLICY_LINK = 'https://adguard.com/privacy/home.html';
|
||||
|
||||
export const LANGUAGES = [
|
||||
{
|
||||
key: 'en',
|
||||
name: 'English',
|
||||
},
|
||||
{
|
||||
key: 'da',
|
||||
name: 'Dansk',
|
||||
@@ -46,6 +42,10 @@ export const LANGUAGES = [
|
||||
key: 'nl',
|
||||
name: 'Dutch',
|
||||
},
|
||||
{
|
||||
key: 'en',
|
||||
name: 'English',
|
||||
},
|
||||
{
|
||||
key: 'es',
|
||||
name: 'Español',
|
||||
|
||||
@@ -3,6 +3,8 @@ import { reactI18nextModule } from 'react-i18next';
|
||||
import { initReactI18n } from 'react-i18next/hooks';
|
||||
import langDetect from 'i18next-browser-languagedetector';
|
||||
|
||||
import { DEFAULT_LANGUAGE } from './helpers/constants';
|
||||
|
||||
import vi from './__locales/vi.json';
|
||||
import en from './__locales/en.json';
|
||||
import ru from './__locales/ru.json';
|
||||
@@ -49,16 +51,16 @@ const resources = {
|
||||
sv: {
|
||||
translation: sv,
|
||||
},
|
||||
'pt-BR': {
|
||||
'pt-br': {
|
||||
translation: ptBR,
|
||||
},
|
||||
'zh-TW': {
|
||||
'zh-tw': {
|
||||
translation: zhTW,
|
||||
},
|
||||
bg: {
|
||||
translation: bg,
|
||||
},
|
||||
'zh-CN': {
|
||||
'zh-cn': {
|
||||
translation: zhCN,
|
||||
},
|
||||
cs: {
|
||||
@@ -85,7 +87,7 @@ const resources = {
|
||||
pl: {
|
||||
translation: pl,
|
||||
},
|
||||
'pt-PT': {
|
||||
'pt-pt': {
|
||||
translation: ptPT,
|
||||
},
|
||||
sk: {
|
||||
@@ -99,22 +101,29 @@ const resources = {
|
||||
},
|
||||
};
|
||||
|
||||
const availableLanguages = Object.keys(resources);
|
||||
|
||||
i18n
|
||||
.use(langDetect)
|
||||
.use(initReactI18n)
|
||||
.use(reactI18nextModule) // passes i18n down to react-i18next
|
||||
.use(reactI18nextModule)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
keySeparator: false, // we use content as keys
|
||||
nsSeparator: false, // Fix character in content
|
||||
returnEmptyString: false, // count empty value as invalid
|
||||
lowerCaseLng: true,
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
keySeparator: false,
|
||||
nsSeparator: false,
|
||||
returnEmptyString: false,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react!!
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
wait: true,
|
||||
},
|
||||
}, () => {
|
||||
if (!availableLanguages.includes(i18n.language)) {
|
||||
i18n.changeLanguage(DEFAULT_LANGUAGE);
|
||||
}
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -189,6 +189,14 @@ const dashboard = handleActions(
|
||||
processingDnsSettings: false,
|
||||
};
|
||||
},
|
||||
|
||||
[actions.getProfileRequest]: state => ({ ...state, processingProfile: true }),
|
||||
[actions.getProfileFailure]: state => ({ ...state, processingProfile: false }),
|
||||
[actions.getProfileSuccess]: (state, { payload }) => ({
|
||||
...state,
|
||||
name: payload.name,
|
||||
processingProfile: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
processing: true,
|
||||
@@ -198,6 +206,7 @@ const dashboard = handleActions(
|
||||
processingClients: true,
|
||||
processingUpdate: false,
|
||||
processingDnsSettings: true,
|
||||
processingProfile: true,
|
||||
upstreamDns: '',
|
||||
bootstrapDns: '',
|
||||
allServers: false,
|
||||
@@ -209,6 +218,7 @@ const dashboard = handleActions(
|
||||
dnsVersion: '',
|
||||
clients: [],
|
||||
autoClients: [],
|
||||
name: '',
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -772,18 +773,31 @@ func (d *Dnsfilter) initFiltering(filters map[int]string) error {
|
||||
list = &urlfilter.StringRuleList{
|
||||
ID: 0,
|
||||
RulesText: dataOrFilePath,
|
||||
IgnoreCosmetic: false,
|
||||
IgnoreCosmetic: true,
|
||||
}
|
||||
|
||||
} else if !fileExists(dataOrFilePath) {
|
||||
list = &urlfilter.StringRuleList{
|
||||
ID: id,
|
||||
IgnoreCosmetic: false,
|
||||
IgnoreCosmetic: true,
|
||||
}
|
||||
|
||||
} else if runtime.GOOS == "windows" {
|
||||
// On Windows we don't pass a file to urlfilter because
|
||||
// it's difficult to update this file while it's being used.
|
||||
data, err := ioutil.ReadFile(dataOrFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.ReadFile(): %s: %s", dataOrFilePath, err)
|
||||
}
|
||||
list = &urlfilter.StringRuleList{
|
||||
ID: id,
|
||||
RulesText: string(data),
|
||||
IgnoreCosmetic: true,
|
||||
}
|
||||
|
||||
} else {
|
||||
var err error
|
||||
list, err = urlfilter.NewFileRuleList(id, dataOrFilePath, false)
|
||||
list, err = urlfilter.NewFileRuleList(id, dataOrFilePath, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("urlfilter.NewFileRuleList(): %s: %s", dataOrFilePath, err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -302,6 +303,12 @@ func (s *Server) Reconfigure(config *ServerConfig) error {
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "could not reconfigure the server")
|
||||
}
|
||||
|
||||
// On some Windows versions the UDP port we've just closed in proxy.Stop() doesn't get actually closed right away.
|
||||
if runtime.GOOS == "windows" {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
err = s.startInternal(config)
|
||||
if err != nil {
|
||||
return errorx.Decorate(err, "could not reconfigure the server")
|
||||
@@ -425,7 +432,6 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
|
||||
if res.Reason == dnsfilter.ReasonRewrite && len(res.CanonName) != 0 {
|
||||
d.Req.Question[0] = originalQuestion
|
||||
d.Res.Question[0] = originalQuestion
|
||||
|
||||
if len(d.Res.Answer) != 0 {
|
||||
answer = append(answer, d.Res.Answer...) // host -> IP
|
||||
@@ -434,6 +440,10 @@ func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
|
||||
}
|
||||
}
|
||||
|
||||
if d.Res != nil {
|
||||
d.Res.Compress = true // some devices require DNS message compression
|
||||
}
|
||||
|
||||
shouldLog := true
|
||||
msg := d.Req
|
||||
|
||||
|
||||
119
home/auth.go
119
home/auth.go
@@ -20,10 +20,44 @@ import (
|
||||
const cookieTTL = 365 * 24 // in hours
|
||||
const expireTime = 30 * 24 // in hours
|
||||
|
||||
type session struct {
|
||||
userName string
|
||||
expire uint32 // expiration time (in seconds)
|
||||
}
|
||||
|
||||
/*
|
||||
expire byte[4]
|
||||
name_len byte[2]
|
||||
name byte[]
|
||||
*/
|
||||
func (s *session) serialize() []byte {
|
||||
var data []byte
|
||||
data = make([]byte, 4+2+len(s.userName))
|
||||
binary.BigEndian.PutUint32(data[0:4], s.expire)
|
||||
binary.BigEndian.PutUint16(data[4:6], uint16(len(s.userName)))
|
||||
copy(data[6:], []byte(s.userName))
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *session) deserialize(data []byte) bool {
|
||||
if len(data) < 4+2 {
|
||||
return false
|
||||
}
|
||||
s.expire = binary.BigEndian.Uint32(data[0:4])
|
||||
nameLen := binary.BigEndian.Uint16(data[4:6])
|
||||
data = data[6:]
|
||||
|
||||
if len(data) < int(nameLen) {
|
||||
return false
|
||||
}
|
||||
s.userName = string(data)
|
||||
return true
|
||||
}
|
||||
|
||||
// Auth - global object
|
||||
type Auth struct {
|
||||
db *bbolt.DB
|
||||
sessions map[string]uint32 // session -> expiration time (in seconds)
|
||||
sessions map[string]*session // session name -> session data
|
||||
lock sync.Mutex
|
||||
users []User
|
||||
}
|
||||
@@ -37,7 +71,7 @@ type User struct {
|
||||
// InitAuth - create a global object
|
||||
func InitAuth(dbFilename string, users []User) *Auth {
|
||||
a := Auth{}
|
||||
a.sessions = make(map[string]uint32)
|
||||
a.sessions = make(map[string]*session)
|
||||
rand.Seed(time.Now().UTC().Unix())
|
||||
var err error
|
||||
a.db, err = bbolt.Open(dbFilename, 0644, nil)
|
||||
@@ -56,6 +90,10 @@ func (a *Auth) Close() {
|
||||
_ = a.db.Close()
|
||||
}
|
||||
|
||||
func bucketName() []byte {
|
||||
return []byte("sessions-2")
|
||||
}
|
||||
|
||||
// load sessions from file, remove expired sessions
|
||||
func (a *Auth) loadSessions() {
|
||||
tx, err := a.db.Begin(true)
|
||||
@@ -67,16 +105,22 @@ func (a *Auth) loadSessions() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte("sessions"))
|
||||
bkt := tx.Bucket(bucketName())
|
||||
if bkt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
removed := 0
|
||||
|
||||
if tx.Bucket([]byte("sessions")) != nil {
|
||||
_ = tx.DeleteBucket([]byte("sessions"))
|
||||
removed = 1
|
||||
}
|
||||
|
||||
now := uint32(time.Now().UTC().Unix())
|
||||
forEach := func(k, v []byte) error {
|
||||
i := binary.BigEndian.Uint32(v)
|
||||
if i <= now {
|
||||
s := session{}
|
||||
if !s.deserialize(v) || s.expire <= now {
|
||||
err = bkt.Delete(k)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Delete: %s", err)
|
||||
@@ -85,7 +129,8 @@ func (a *Auth) loadSessions() {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
a.sessions[hex.EncodeToString(k)] = i
|
||||
|
||||
a.sessions[hex.EncodeToString(k)] = &s
|
||||
return nil
|
||||
}
|
||||
_ = bkt.ForEach(forEach)
|
||||
@@ -99,11 +144,15 @@ func (a *Auth) loadSessions() {
|
||||
}
|
||||
|
||||
// store session data in file
|
||||
func (a *Auth) storeSession(data []byte, expire uint32) {
|
||||
func (a *Auth) addSession(data []byte, s *session) {
|
||||
a.lock.Lock()
|
||||
a.sessions[hex.EncodeToString(data)] = expire
|
||||
a.sessions[hex.EncodeToString(data)] = s
|
||||
a.lock.Unlock()
|
||||
a.storeSession(data, s)
|
||||
}
|
||||
|
||||
// store session data in file
|
||||
func (a *Auth) storeSession(data []byte, s *session) {
|
||||
tx, err := a.db.Begin(true)
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Begin: %s", err)
|
||||
@@ -113,15 +162,12 @@ func (a *Auth) storeSession(data []byte, expire uint32) {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt, err := tx.CreateBucketIfNotExists([]byte("sessions"))
|
||||
bkt, err := tx.CreateBucketIfNotExists(bucketName())
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.CreateBucketIfNotExists: %s", err)
|
||||
return
|
||||
}
|
||||
var val []byte
|
||||
val = make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(val, expire)
|
||||
err = bkt.Put(data, val)
|
||||
err = bkt.Put(data, s.serialize())
|
||||
if err != nil {
|
||||
log.Error("Auth: bbolt.Put: %s", err)
|
||||
return
|
||||
@@ -147,7 +193,7 @@ func (a *Auth) removeSession(sess []byte) {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
bkt := tx.Bucket([]byte("sessions"))
|
||||
bkt := tx.Bucket(bucketName())
|
||||
if bkt == nil {
|
||||
log.Error("Auth: bbolt.Bucket")
|
||||
return
|
||||
@@ -174,12 +220,12 @@ func (a *Auth) CheckSession(sess string) int {
|
||||
update := false
|
||||
|
||||
a.lock.Lock()
|
||||
expire, ok := a.sessions[sess]
|
||||
s, ok := a.sessions[sess]
|
||||
if !ok {
|
||||
a.lock.Unlock()
|
||||
return -1
|
||||
}
|
||||
if expire <= now {
|
||||
if s.expire <= now {
|
||||
delete(a.sessions, sess)
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.removeSession(key)
|
||||
@@ -188,17 +234,17 @@ func (a *Auth) CheckSession(sess string) int {
|
||||
}
|
||||
|
||||
newExpire := now + expireTime*60*60
|
||||
if expire/(24*60*60) != newExpire/(24*60*60) {
|
||||
if s.expire/(24*60*60) != newExpire/(24*60*60) {
|
||||
// update expiration time once a day
|
||||
update = true
|
||||
a.sessions[sess] = newExpire
|
||||
s.expire = newExpire
|
||||
}
|
||||
|
||||
a.lock.Unlock()
|
||||
|
||||
if update {
|
||||
key, _ := hex.DecodeString(sess)
|
||||
a.storeSession(key, expire)
|
||||
a.storeSession(key, s)
|
||||
}
|
||||
|
||||
return 0
|
||||
@@ -238,8 +284,10 @@ func httpCookie(req loginJSON) string {
|
||||
expstr = expstr[:len(expstr)-len("UTC")] // "UTC" -> "GMT"
|
||||
expstr += "GMT"
|
||||
|
||||
expireSess := uint32(now.Unix()) + expireTime*60*60
|
||||
config.auth.storeSession(sess, expireSess)
|
||||
s := session{}
|
||||
s.userName = u.Name
|
||||
s.expire = uint32(now.Unix()) + expireTime*60*60
|
||||
config.auth.addSession(sess, &s)
|
||||
|
||||
return fmt.Sprintf("session=%s; Path=/; HttpOnly; Expires=%s", hex.EncodeToString(sess), expstr)
|
||||
}
|
||||
@@ -402,6 +450,35 @@ func (a *Auth) UserFind(login string, password string) User {
|
||||
return User{}
|
||||
}
|
||||
|
||||
// GetCurrentUser - get the current user
|
||||
func (a *Auth) GetCurrentUser(r *http.Request) User {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
// there's no Cookie, check Basic authentication
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if ok {
|
||||
u := config.auth.UserFind(user, pass)
|
||||
return u
|
||||
}
|
||||
return User{}
|
||||
}
|
||||
|
||||
a.lock.Lock()
|
||||
s, ok := a.sessions[cookie.Value]
|
||||
if !ok {
|
||||
a.lock.Unlock()
|
||||
return User{}
|
||||
}
|
||||
for _, u := range a.users {
|
||||
if u.Name == s.userName {
|
||||
a.lock.Unlock()
|
||||
return u
|
||||
}
|
||||
}
|
||||
a.lock.Unlock()
|
||||
return User{}
|
||||
}
|
||||
|
||||
// GetUsers - get users
|
||||
func (a *Auth) GetUsers() []User {
|
||||
a.lock.Lock()
|
||||
|
||||
@@ -28,6 +28,7 @@ func TestAuth(t *testing.T) {
|
||||
User{Name: "name", PasswordHash: "$2y$05$..vyzAECIhJPfaQiOK17IukcQnqEgKJHy0iETyYqxn3YXJl8yZuo2"},
|
||||
}
|
||||
a := InitAuth(fn, nil)
|
||||
s := session{}
|
||||
|
||||
user := User{Name: "name"}
|
||||
a.UserAdd(&user, "password")
|
||||
@@ -38,12 +39,16 @@ func TestAuth(t *testing.T) {
|
||||
sess := getSession(&users[0])
|
||||
sessStr := hex.EncodeToString(sess)
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
// check expiration
|
||||
a.storeSession(sess, uint32(time.Now().UTC().Unix()))
|
||||
s.expire = uint32(now)
|
||||
a.addSession(sess, &s)
|
||||
assert.True(t, a.CheckSession(sessStr) == 1)
|
||||
|
||||
// add session with TTL = 2 sec
|
||||
a.storeSession(sess, uint32(time.Now().UTC().Unix()+2))
|
||||
s = session{}
|
||||
s.expire = uint32(now + 2)
|
||||
a.addSession(sess, &s)
|
||||
assert.True(t, a.CheckSession(sessStr) == 0)
|
||||
|
||||
a.Close()
|
||||
@@ -53,6 +58,9 @@ func TestAuth(t *testing.T) {
|
||||
|
||||
// the session is still alive
|
||||
assert.True(t, a.CheckSession(sessStr) == 0)
|
||||
// reset our expiration time because CheckSession() has just updated it
|
||||
s.expire = uint32(now + 2)
|
||||
a.storeSession(sess, &s)
|
||||
a.Close()
|
||||
|
||||
u := a.UserFind("name", "password")
|
||||
|
||||
@@ -377,6 +377,23 @@ func checkDNS(input string, bootstrap []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type profileJSON struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
pj := profileJSON{}
|
||||
u := config.auth.GetCurrentUser(r)
|
||||
pj.Name = u.Name
|
||||
|
||||
data, err := json.Marshal(pj)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
// --------------
|
||||
// DNS-over-HTTPS
|
||||
// --------------
|
||||
@@ -416,6 +433,7 @@ func registerControlHandlers() {
|
||||
|
||||
httpRegister(http.MethodGet, "/control/access/list", handleAccessList)
|
||||
httpRegister(http.MethodPost, "/control/access/set", handleAccessSet)
|
||||
httpRegister("GET", "/control/profile", handleGetProfile)
|
||||
|
||||
RegisterFilteringHandlers()
|
||||
RegisterTLSHandlers()
|
||||
|
||||
@@ -156,7 +156,7 @@ func handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func verifyCertChain(data *tlsConfigStatus, certChain string, serverName string) error {
|
||||
log.Tracef("got certificate: %s", certChain)
|
||||
log.Tracef("TLS: got certificate: %d bytes", len(certChain))
|
||||
|
||||
// now do a more extended validation
|
||||
var certs []*pem.Block // PEM-encoded certificates
|
||||
|
||||
@@ -59,7 +59,6 @@ type getVersionJSONRequest struct {
|
||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if config.disableUpdate {
|
||||
httpError(w, http.StatusInternalServerError, "New app version check is disabled by user")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/joomcode/errorx"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type dnsContext struct {
|
||||
@@ -137,11 +136,6 @@ func isPublicIP(ip net.IP) bool {
|
||||
}
|
||||
|
||||
func onDNSRequest(d *proxy.DNSContext) {
|
||||
qType := d.Req.Question[0].Qtype
|
||||
if qType != dns.TypeA && qType != dns.TypeAAAA {
|
||||
return
|
||||
}
|
||||
|
||||
ip := dnsforward.GetIPString(d.Addr)
|
||||
if ip == "" {
|
||||
// This would be quite weird if we get here
|
||||
|
||||
@@ -217,8 +217,12 @@ func refreshFilters() (int, error) {
|
||||
// . For each filter run the download and checksum check operation
|
||||
// . For each filter:
|
||||
// . If filter data hasn't changed, just set new update time on file
|
||||
// . If filter data has changed: rename the old file, store the new data on disk
|
||||
// . Pass new filters to dnsfilter object
|
||||
// . If filter data has changed:
|
||||
// . rename the old file (1.txt -> 1.txt.old)
|
||||
// . store the new data on disk (1.txt)
|
||||
// . Pass new filters to dnsfilter object - it analyzes new data while the old filters are still active
|
||||
// . dnsfilter activates new filters
|
||||
// . Remove the old filter files (1.txt.old)
|
||||
func refreshFiltersIfNecessary(force bool) int {
|
||||
var updateFilters []filter
|
||||
var updateFlags []bool // 'true' if filter data has changed
|
||||
@@ -431,7 +435,10 @@ func (filter *filter) save() error {
|
||||
|
||||
func (filter *filter) saveAndBackupOld() error {
|
||||
filterFilePath := filter.Path()
|
||||
_ = os.Rename(filterFilePath, filterFilePath+".old")
|
||||
err := os.Rename(filterFilePath, filterFilePath+".old")
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return filter.save()
|
||||
}
|
||||
|
||||
|
||||
54
home/rdns.go
54
home/rdns.go
@@ -1,12 +1,13 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -18,12 +19,14 @@ const (
|
||||
// RDNS - module context
|
||||
type RDNS struct {
|
||||
clients *clientsContainer
|
||||
ipChannel chan string // pass data from DNS request handling thread to rDNS thread
|
||||
// contains IP addresses of clients to be resolved by rDNS
|
||||
// if IP address couldn't be resolved, it stays here forever to prevent further attempts to resolve the same IP
|
||||
ips map[string]bool
|
||||
lock sync.Mutex // synchronize access to 'ips'
|
||||
upstream upstream.Upstream // Upstream object for our own DNS server
|
||||
ipChannel chan string // pass data from DNS request handling thread to rDNS thread
|
||||
upstream upstream.Upstream // Upstream object for our own DNS server
|
||||
|
||||
// Contains IP addresses of clients to be resolved by rDNS
|
||||
// If IP address is resolved, it stays here while it's inside Clients.
|
||||
// If it's removed from Clients, this IP address will be resolved once again.
|
||||
// If IP address couldn't be resolved, it stays here for some time to prevent further attempts to resolve the same IP.
|
||||
ipAddrs cache.Cache
|
||||
}
|
||||
|
||||
// InitRDNS - create module context
|
||||
@@ -47,7 +50,11 @@ func InitRDNS(clients *clientsContainer) *RDNS {
|
||||
return nil
|
||||
}
|
||||
|
||||
r.ips = make(map[string]bool)
|
||||
cconf := cache.Config{}
|
||||
cconf.EnableLRU = true
|
||||
cconf.MaxCount = 10000
|
||||
r.ipAddrs = cache.New(cconf)
|
||||
|
||||
r.ipChannel = make(chan string, 256)
|
||||
go r.workerLoop()
|
||||
return &r
|
||||
@@ -55,25 +62,30 @@ func InitRDNS(clients *clientsContainer) *RDNS {
|
||||
|
||||
// Begin - add IP address to rDNS queue
|
||||
func (r *RDNS) Begin(ip string) {
|
||||
now := uint64(time.Now().Unix())
|
||||
expire := r.ipAddrs.Get([]byte(ip))
|
||||
if len(expire) != 0 {
|
||||
exp := binary.BigEndian.Uint64(expire)
|
||||
if exp > now {
|
||||
return
|
||||
}
|
||||
// TTL expired
|
||||
}
|
||||
expire = make([]byte, 8)
|
||||
const ttl = 12 * 60 * 60
|
||||
binary.BigEndian.PutUint64(expire, now+ttl)
|
||||
_ = r.ipAddrs.Set([]byte(ip), expire)
|
||||
|
||||
if r.clients.Exists(ip, ClientSourceRDNS) {
|
||||
return
|
||||
}
|
||||
|
||||
// add IP to ips, if not exists
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
_, ok := r.ips[ip]
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
r.ips[ip] = true
|
||||
|
||||
log.Tracef("Adding %s for rDNS resolve", ip)
|
||||
log.Tracef("rDNS: adding %s", ip)
|
||||
select {
|
||||
case r.ipChannel <- ip:
|
||||
//
|
||||
default:
|
||||
log.Tracef("rDNS queue is full")
|
||||
log.Tracef("rDNS: queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +144,6 @@ func (r *RDNS) workerLoop() {
|
||||
continue
|
||||
}
|
||||
|
||||
r.lock.Lock()
|
||||
delete(r.ips, ip)
|
||||
r.lock.Unlock()
|
||||
|
||||
_, _ = config.clients.AddHost(ip, host, ClientSourceRDNS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
@@ -20,10 +21,13 @@ const (
|
||||
// Whois - module context
|
||||
type Whois struct {
|
||||
clients *clientsContainer
|
||||
ips map[string]bool
|
||||
lock sync.Mutex
|
||||
ipChan chan string
|
||||
timeoutMsec uint
|
||||
|
||||
// Contains IP addresses of clients
|
||||
// An active IP address is resolved once again after it expires.
|
||||
// If IP address couldn't be resolved, it stays here for some time to prevent further attempts to resolve the same IP.
|
||||
ipAddrs cache.Cache
|
||||
}
|
||||
|
||||
// Create module context
|
||||
@@ -31,7 +35,12 @@ func initWhois(clients *clientsContainer) *Whois {
|
||||
w := Whois{}
|
||||
w.timeoutMsec = 5000
|
||||
w.clients = clients
|
||||
w.ips = make(map[string]bool)
|
||||
|
||||
cconf := cache.Config{}
|
||||
cconf.EnableLRU = true
|
||||
cconf.MaxCount = 10000
|
||||
w.ipAddrs = cache.New(cconf)
|
||||
|
||||
w.ipChan = make(chan string, 255)
|
||||
go w.workerLoop()
|
||||
return &w
|
||||
@@ -186,14 +195,19 @@ func (w *Whois) process(ip string) [][]string {
|
||||
|
||||
// Begin - begin requesting WHOIS info
|
||||
func (w *Whois) Begin(ip string) {
|
||||
w.lock.Lock()
|
||||
_, found := w.ips[ip]
|
||||
if found {
|
||||
w.lock.Unlock()
|
||||
return
|
||||
now := uint64(time.Now().Unix())
|
||||
expire := w.ipAddrs.Get([]byte(ip))
|
||||
if len(expire) != 0 {
|
||||
exp := binary.BigEndian.Uint64(expire)
|
||||
if exp > now {
|
||||
return
|
||||
}
|
||||
// TTL expired
|
||||
}
|
||||
w.ips[ip] = true
|
||||
w.lock.Unlock()
|
||||
expire = make([]byte, 8)
|
||||
const ttl = 12 * 60 * 60
|
||||
binary.BigEndian.PutUint64(expire, now+ttl)
|
||||
_ = w.ipAddrs.Set([]byte(ip), expire)
|
||||
|
||||
log.Debug("Whois: adding %s", ip)
|
||||
select {
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
# AdGuard Home API Change Log
|
||||
|
||||
|
||||
## v0.99.1: API changes
|
||||
|
||||
### API: Get current user info: GET /control/profile
|
||||
|
||||
Request:
|
||||
|
||||
GET /control/profile
|
||||
|
||||
Response:
|
||||
|
||||
200 OK
|
||||
|
||||
{
|
||||
"name":"..."
|
||||
}
|
||||
|
||||
|
||||
## v0.99: incompatible API changes
|
||||
|
||||
* A note about web user authentication
|
||||
|
||||
@@ -970,6 +970,18 @@ paths:
|
||||
302:
|
||||
description: OK
|
||||
|
||||
/profile:
|
||||
get:
|
||||
tags:
|
||||
- global
|
||||
operationId: getProfile
|
||||
summary: ""
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: "#/definitions/ProfileInfo"
|
||||
|
||||
definitions:
|
||||
ServerStatus:
|
||||
type: "object"
|
||||
@@ -1559,6 +1571,14 @@ definitions:
|
||||
description: "Network interfaces dictionary (key is the interface name)"
|
||||
additionalProperties:
|
||||
$ref: "#/definitions/NetInterface"
|
||||
|
||||
ProfileInfo:
|
||||
type: "object"
|
||||
description: "Information about the current user"
|
||||
properties:
|
||||
name:
|
||||
type: "string"
|
||||
|
||||
Client:
|
||||
type: "object"
|
||||
description: "Client information"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package querylog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
|
||||
const enableGzip = false
|
||||
@@ -61,12 +61,7 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
elapsed := time.Since(start)
|
||||
log.Debug("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
|
||||
|
||||
err := checkBuffer(buffer, b)
|
||||
if err != nil {
|
||||
log.Error("failed to check buffer: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var zb bytes.Buffer
|
||||
filename := l.logFile
|
||||
|
||||
@@ -113,34 +108,6 @@ func (l *queryLog) flushToFile(buffer []*logEntry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
|
||||
l := len(buffer)
|
||||
d := json.NewDecoder(&b)
|
||||
|
||||
i := 0
|
||||
for d.More() {
|
||||
entry := &logEntry{}
|
||||
err := d.Decode(entry)
|
||||
if err != nil {
|
||||
log.Error("Failed to decode: %s", err)
|
||||
return err
|
||||
}
|
||||
if diff := deep.Equal(entry, buffer[i]); diff != nil {
|
||||
log.Error("decoded buffer differs: %s", diff)
|
||||
return fmt.Errorf("decoded buffer differs: %s", diff)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i != l {
|
||||
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
|
||||
log.Error("%v", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("check ok: %d entries", i)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *queryLog) rotate() error {
|
||||
from := l.logFile
|
||||
to := l.logFile + ".1"
|
||||
@@ -181,7 +148,7 @@ type Reader struct {
|
||||
ql *queryLog
|
||||
|
||||
f *os.File
|
||||
jd *json.Decoder
|
||||
reader *bufio.Reader // reads file line by line
|
||||
now time.Time
|
||||
validFrom int64 // UNIX time (ns)
|
||||
olderThan int64 // UNIX time (ns)
|
||||
@@ -250,7 +217,6 @@ func (r *Reader) BeginRead(olderThan time.Time, count uint64) {
|
||||
}
|
||||
r.filePrepared = false
|
||||
r.searching = false
|
||||
r.jd = nil
|
||||
}
|
||||
|
||||
// BeginReadPrev - start reading the previous data chunk
|
||||
@@ -280,7 +246,6 @@ func (r *Reader) BeginReadPrev(olderThan time.Time, count uint64) {
|
||||
|
||||
r.filePrepared = true
|
||||
r.searching = false
|
||||
r.jd = nil
|
||||
}
|
||||
|
||||
// Perform binary seek
|
||||
@@ -314,27 +279,17 @@ func (fs *fileSeeker) seekBinary(cur uint64) int32 {
|
||||
|
||||
// Seek to a new line
|
||||
func (r *Reader) seekToNewLine() bool {
|
||||
b := make([]byte, maxEntrySize*2)
|
||||
|
||||
_, err := r.f.Read(b)
|
||||
r.reader = bufio.NewReader(r.f)
|
||||
b, err := r.reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
r.reader = nil
|
||||
log.Error("QueryLog: file.Read: %s: %s", r.files[r.ifile], err)
|
||||
return false
|
||||
}
|
||||
|
||||
off := bytes.IndexByte(b, '\n') + 1
|
||||
if off == 0 {
|
||||
log.Error("QueryLog: Can't find a new line: %s", r.files[r.ifile])
|
||||
return false
|
||||
}
|
||||
|
||||
off := len(b)
|
||||
r.fpos += uint64(off)
|
||||
log.Debug("QueryLog: seek: %x (+%d)", r.fpos, off)
|
||||
_, err = r.f.Seek(int64(r.fpos), io.SeekStart)
|
||||
if err != nil {
|
||||
log.Error("QueryLog: file.Seek: %s: %s", r.files[r.ifile], err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -405,7 +360,6 @@ func (r *Reader) prepareRead() bool {
|
||||
|
||||
// Next - return the next entry or nil if reading is finished
|
||||
func (r *Reader) Next() *logEntry { // nolint
|
||||
var err error
|
||||
for {
|
||||
// open file if needed
|
||||
if r.f == nil {
|
||||
@@ -425,30 +379,26 @@ func (r *Reader) Next() *logEntry { // nolint
|
||||
r.filePrepared = true
|
||||
}
|
||||
|
||||
// open decoder if needed
|
||||
if r.jd == nil {
|
||||
r.jd = json.NewDecoder(r.f)
|
||||
}
|
||||
|
||||
// check if there's data
|
||||
if !r.jd.More() {
|
||||
r.jd = nil
|
||||
// open decoder
|
||||
b, err := r.reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
strReader := strings.NewReader(string(b))
|
||||
jd := json.NewDecoder(strReader)
|
||||
|
||||
// read data
|
||||
var entry logEntry
|
||||
err = r.jd.Decode(&entry)
|
||||
err = jd.Decode(&entry)
|
||||
if err != nil {
|
||||
log.Error("QueryLog: Failed to decode: %s", err)
|
||||
r.jd = nil
|
||||
return nil
|
||||
log.Debug("QueryLog: Failed to decode: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
t := entry.Time.UnixNano()
|
||||
if r.searching {
|
||||
r.jd = nil
|
||||
|
||||
r.reader = nil
|
||||
rr := r.fseeker.seekBinary(uint64(t))
|
||||
r.fpos = r.fseeker.pos
|
||||
if rr < 0 {
|
||||
|
||||
@@ -41,6 +41,7 @@ func (s *statsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
@@ -94,16 +95,8 @@ func (s *statsCtx) initWeb() {
|
||||
return
|
||||
}
|
||||
|
||||
s.conf.HTTPRegister("GET", "/control/stats", func(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleStats(w, r)
|
||||
})
|
||||
s.conf.HTTPRegister("POST", "/control/stats_reset", func(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleStatsReset(w, r)
|
||||
})
|
||||
s.conf.HTTPRegister("POST", "/control/stats_config", func(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleStatsConfig(w, r)
|
||||
})
|
||||
s.conf.HTTPRegister("GET", "/control/stats_info", func(w http.ResponseWriter, r *http.Request) {
|
||||
s.handleStatsInfo(w, r)
|
||||
})
|
||||
s.conf.HTTPRegister("GET", "/control/stats", s.handleStats)
|
||||
s.conf.HTTPRegister("POST", "/control/stats_reset", s.handleStatsReset)
|
||||
s.conf.HTTPRegister("POST", "/control/stats_config", s.handleStatsConfig)
|
||||
s.conf.HTTPRegister("GET", "/control/stats_info", s.handleStatsInfo)
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ func (s *statsCtx) loadUnitFromDB(tx *bolt.Tx, id uint32) *unitDB {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Tracef("Loading unit %d", id)
|
||||
// log.Tracef("Loading unit %d", id)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Write(bkt.Get([]byte{0}))
|
||||
|
||||
Reference in New Issue
Block a user